mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-12 04:29:44 +02:00
Merge pull request #1200 from cisagov/dk/1016-nameservers-ui
Issue 1016 - Nameserver UI (includes issues 919 and 1104)
This commit is contained in:
commit
228da2aafa
14 changed files with 549 additions and 143 deletions
|
@ -45,7 +45,7 @@ except NameError:
|
||||||
# Attn: these imports should NOT be at the top of the file
|
# Attn: these imports should NOT be at the top of the file
|
||||||
try:
|
try:
|
||||||
from .client import CLIENT, commands
|
from .client import CLIENT, commands
|
||||||
from .errors import RegistryError, ErrorCode, CANNOT_CONTACT_REGISTRY, GENERIC_ERROR
|
from .errors import RegistryError, ErrorCode
|
||||||
from epplib.models import common, info
|
from epplib.models import common, info
|
||||||
from epplib.responses import extensions
|
from epplib.responses import extensions
|
||||||
from epplib import responses
|
from epplib import responses
|
||||||
|
@ -61,6 +61,4 @@ __all__ = [
|
||||||
"info",
|
"info",
|
||||||
"ErrorCode",
|
"ErrorCode",
|
||||||
"RegistryError",
|
"RegistryError",
|
||||||
"CANNOT_CONTACT_REGISTRY",
|
|
||||||
"GENERIC_ERROR",
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
|
||||||
CANNOT_CONTACT_REGISTRY = "Update failed. Cannot contact the registry."
|
|
||||||
GENERIC_ERROR = "Value entered was wrong."
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorCode(IntEnum):
|
class ErrorCode(IntEnum):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -231,41 +231,15 @@ function handleValidationClick(e) {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An IIFE that attaches a click handler for our dynamic nameservers form
|
* Prepare the namerservers and DS data forms delete buttons
|
||||||
|
* We will call this on the forms init, and also every time we add a form
|
||||||
*
|
*
|
||||||
* Only does something on a single page, but it should be fast enough to run
|
|
||||||
* it everywhere.
|
|
||||||
*/
|
*/
|
||||||
(function prepareNameserverForms() {
|
function prepareDeleteButtons(formLabel) {
|
||||||
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;
|
|
||||||
if (addButton)
|
|
||||||
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');
|
|
||||||
|
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
function prepareDeleteButtons() {
|
|
||||||
let deleteButtons = document.querySelectorAll(".delete-record");
|
let deleteButtons = document.querySelectorAll(".delete-record");
|
||||||
let totalForms = document.querySelector("#id_form-TOTAL_FORMS");
|
let totalForms = document.querySelector("#id_form-TOTAL_FORMS");
|
||||||
|
let isNameserversForm = document.title.includes("DNS name servers |");
|
||||||
|
let addButton = document.querySelector("#add-form");
|
||||||
|
|
||||||
// Loop through each delete button and attach the click event listener
|
// Loop through each delete button and attach the click event listener
|
||||||
deleteButtons.forEach((deleteButton) => {
|
deleteButtons.forEach((deleteButton) => {
|
||||||
|
@ -273,13 +247,15 @@ function prepareDeleteButtons() {
|
||||||
});
|
});
|
||||||
|
|
||||||
function removeForm(e){
|
function removeForm(e){
|
||||||
let formToRemove = e.target.closest(".ds-record");
|
let formToRemove = e.target.closest(".repeatable-form");
|
||||||
formToRemove.remove();
|
formToRemove.remove();
|
||||||
let forms = document.querySelectorAll(".ds-record");
|
let forms = document.querySelectorAll(".repeatable-form");
|
||||||
totalForms.setAttribute('value', `${forms.length}`);
|
totalForms.setAttribute('value', `${forms.length}`);
|
||||||
|
|
||||||
let formNumberRegex = RegExp(`form-(\\d){1}-`, 'g');
|
let formNumberRegex = RegExp(`form-(\\d){1}-`, 'g');
|
||||||
let formLabelRegex = RegExp(`DS Data record (\\d){1}`, 'g');
|
let formLabelRegex = RegExp(`${formLabel} (\\d+){1}`, 'g');
|
||||||
|
// For the example on Nameservers
|
||||||
|
let formExampleRegex = RegExp(`ns(\\d+){1}`, 'g');
|
||||||
|
|
||||||
forms.forEach((form, index) => {
|
forms.forEach((form, index) => {
|
||||||
// Iterate over child nodes of the current element
|
// Iterate over child nodes of the current element
|
||||||
|
@ -294,48 +270,88 @@ function prepareDeleteButtons() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Array.from(form.querySelectorAll('h2, legend')).forEach((node) => {
|
// h2 and legend for DS form, label for nameservers
|
||||||
node.textContent = node.textContent.replace(formLabelRegex, `DS Data record ${index + 1}`);
|
Array.from(form.querySelectorAll('h2, legend, label, p')).forEach((node) => {
|
||||||
|
|
||||||
|
// Ticket: 1192
|
||||||
|
// if (isNameserversForm && index <= 1 && !node.innerHTML.includes('*')) {
|
||||||
|
// // Create a new element
|
||||||
|
// const newElement = document.createElement('abbr');
|
||||||
|
// newElement.textContent = '*';
|
||||||
|
// // TODO: finish building abbr
|
||||||
|
|
||||||
|
// // Append the new element to the parent
|
||||||
|
// node.appendChild(newElement);
|
||||||
|
// // Find the next sibling that is an input element
|
||||||
|
// let nextInputElement = node.nextElementSibling;
|
||||||
|
|
||||||
|
// while (nextInputElement) {
|
||||||
|
// if (nextInputElement.tagName === 'INPUT') {
|
||||||
|
// // Found the next input element
|
||||||
|
// console.log(nextInputElement);
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// nextInputElement = nextInputElement.nextElementSibling;
|
||||||
|
// }
|
||||||
|
// nextInputElement.required = true;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Ticket: 1192 - remove if
|
||||||
|
if (!(isNameserversForm && index <= 1)) {
|
||||||
|
node.textContent = node.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`);
|
||||||
|
node.textContent = node.textContent.replace(formExampleRegex, `ns${index + 1}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Display the add more button if we have less than 13 forms
|
||||||
|
if (isNameserversForm && forms.length <= 13) {
|
||||||
|
addButton.classList.remove("display-none")
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An IIFE that attaches a click handler for our dynamic DNSSEC forms
|
* An IIFE that attaches a click handler for our dynamic formsets
|
||||||
*
|
*
|
||||||
|
* Only does something on a few pages, but it should be fast enough to run
|
||||||
|
* it everywhere.
|
||||||
*/
|
*/
|
||||||
(function prepareDNSSECForms() {
|
(function prepareFormsetsForms() {
|
||||||
let serverForm = document.querySelectorAll(".ds-record");
|
let repeatableForm = document.querySelectorAll(".repeatable-form");
|
||||||
let container = document.querySelector("#form-container");
|
let container = document.querySelector("#form-container");
|
||||||
let addButton = document.querySelector("#add-ds-form");
|
let addButton = document.querySelector("#add-form");
|
||||||
let totalForms = document.querySelector("#id_form-TOTAL_FORMS");
|
let totalForms = document.querySelector("#id_form-TOTAL_FORMS");
|
||||||
|
let cloneIndex = 0;
|
||||||
|
let formLabel = '';
|
||||||
|
let isNameserversForm = document.title.includes("DNS name servers |");
|
||||||
|
if (isNameserversForm) {
|
||||||
|
cloneIndex = 2;
|
||||||
|
formLabel = "Name server";
|
||||||
|
} else if ((document.title.includes("DS Data |")) || (document.title.includes("Key Data |"))) {
|
||||||
|
formLabel = "DS Data record";
|
||||||
|
}
|
||||||
|
|
||||||
// Attach click event listener on the delete buttons of the existing forms
|
// Attach click event listener on the delete buttons of the existing forms
|
||||||
prepareDeleteButtons();
|
prepareDeleteButtons(formLabel);
|
||||||
|
|
||||||
// Attack click event listener on the add button
|
|
||||||
if (addButton)
|
if (addButton)
|
||||||
addButton.addEventListener('click', addForm);
|
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){
|
function addForm(e){
|
||||||
let forms = document.querySelectorAll(".ds-record");
|
let forms = document.querySelectorAll(".repeatable-form");
|
||||||
let formNum = forms.length;
|
let formNum = forms.length;
|
||||||
let newForm = serverForm[0].cloneNode(true);
|
let newForm = repeatableForm[cloneIndex].cloneNode(true);
|
||||||
let formNumberRegex = RegExp(`form-(\\d){1}-`,'g');
|
let formNumberRegex = RegExp(`form-(\\d){1}-`,'g');
|
||||||
let formLabelRegex = RegExp(`DS Data record (\\d){1}`, 'g');
|
let formLabelRegex = RegExp(`${formLabel} (\\d){1}`, 'g');
|
||||||
|
// For the eample on Nameservers
|
||||||
|
let formExampleRegex = RegExp(`ns(\\d){1}`, 'g');
|
||||||
|
|
||||||
formNum++;
|
formNum++;
|
||||||
newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum-1}-`);
|
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, `${formLabel} ${formNum}`);
|
||||||
|
newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum}`);
|
||||||
container.insertBefore(newForm, addButton);
|
container.insertBefore(newForm, addButton);
|
||||||
|
|
||||||
let inputs = newForm.querySelectorAll("input");
|
let inputs = newForm.querySelectorAll("input");
|
||||||
|
@ -379,9 +395,13 @@ function prepareDeleteButtons() {
|
||||||
totalForms.setAttribute('value', `${formNum}`);
|
totalForms.setAttribute('value', `${formNum}`);
|
||||||
|
|
||||||
// Attach click event listener on the delete buttons of the new form
|
// Attach click event listener on the delete buttons of the new form
|
||||||
prepareDeleteButtons();
|
prepareDeleteButtons(formLabel);
|
||||||
}
|
|
||||||
|
|
||||||
|
// Hide the add more button if we have 13 forms
|
||||||
|
if (isNameserversForm && formNum == 13) {
|
||||||
|
addButton.classList.add("display-none")
|
||||||
|
}
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -4,6 +4,10 @@
|
||||||
margin-top: units(3);
|
margin-top: units(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usa-form .usa-button.margin-bottom-075 {
|
||||||
|
margin-bottom: units(1.5);
|
||||||
|
}
|
||||||
|
|
||||||
.usa-form .usa-button.margin-top-1 {
|
.usa-form .usa-button.margin-top-1 {
|
||||||
margin-top: units(1);
|
margin-top: units(1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,12 @@ from django.core.validators import MinValueValidator, MaxValueValidator, RegexVa
|
||||||
from django.forms import formset_factory
|
from django.forms import formset_factory
|
||||||
|
|
||||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||||
|
from registrar.utility.errors import (
|
||||||
|
NameserverError,
|
||||||
|
NameserverErrorCodes as nsErrorCodes,
|
||||||
|
)
|
||||||
|
|
||||||
from ..models import Contact, DomainInformation
|
from ..models import Contact, DomainInformation, Domain
|
||||||
from .common import (
|
from .common import (
|
||||||
ALGORITHM_CHOICES,
|
ALGORITHM_CHOICES,
|
||||||
DIGEST_TYPE_CHOICES,
|
DIGEST_TYPE_CHOICES,
|
||||||
|
@ -19,16 +23,78 @@ class DomainAddUserForm(forms.Form):
|
||||||
email = forms.EmailField(label="Email")
|
email = forms.EmailField(label="Email")
|
||||||
|
|
||||||
|
|
||||||
|
class IPAddressField(forms.CharField):
|
||||||
|
def validate(self, value):
|
||||||
|
super().validate(value) # Run the default CharField validation
|
||||||
|
|
||||||
|
|
||||||
class DomainNameserverForm(forms.Form):
|
class DomainNameserverForm(forms.Form):
|
||||||
"""Form for changing nameservers."""
|
"""Form for changing nameservers."""
|
||||||
|
|
||||||
|
domain = forms.CharField(widget=forms.HiddenInput, required=False)
|
||||||
|
|
||||||
server = forms.CharField(label="Name server", strip=True)
|
server = forms.CharField(label="Name server", strip=True)
|
||||||
# when adding IPs to this form ensure they are stripped as well
|
|
||||||
|
ip = forms.CharField(label="IP Address (IPv4 or IPv6)", strip=True, required=False)
|
||||||
|
|
||||||
|
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()
|
||||||
|
self.clean_empty_strings(cleaned_data)
|
||||||
|
server = cleaned_data.get("server", "")
|
||||||
|
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)
|
||||||
|
|
||||||
|
if ip and not server and ip_list:
|
||||||
|
self.add_error("server", NameserverError(code=nsErrorCodes.MISSING_HOST))
|
||||||
|
elif server:
|
||||||
|
self.validate_nameserver_ip_combo(domain, server, ip_list)
|
||||||
|
|
||||||
|
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
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.add_error("ip", str(e))
|
||||||
|
|
||||||
|
|
||||||
NameserverFormset = formset_factory(
|
NameserverFormset = formset_factory(
|
||||||
DomainNameserverForm,
|
DomainNameserverForm,
|
||||||
extra=1,
|
extra=1,
|
||||||
|
max_num=13,
|
||||||
|
validate_max=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -262,8 +262,11 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
"""Creates the host object in the registry
|
"""Creates the host object in the registry
|
||||||
doesn't add the created host to the domain
|
doesn't add the created host to the domain
|
||||||
returns ErrorCode (int)"""
|
returns ErrorCode (int)"""
|
||||||
if addrs is not None:
|
if addrs is not None and addrs != []:
|
||||||
addresses = [epp.Ip(addr=addr) for addr in addrs]
|
addresses = [
|
||||||
|
epp.Ip(addr=addr, ip="v6" if self.is_ipv6(addr) else None)
|
||||||
|
for addr in addrs
|
||||||
|
]
|
||||||
request = commands.CreateHost(name=host, addrs=addresses)
|
request = commands.CreateHost(name=host, addrs=addresses)
|
||||||
else:
|
else:
|
||||||
request = commands.CreateHost(name=host)
|
request = commands.CreateHost(name=host)
|
||||||
|
@ -274,7 +277,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
return response.code
|
return response.code
|
||||||
except RegistryError as e:
|
except RegistryError as e:
|
||||||
logger.error("Error _create_host, code was %s error was %s" % (e.code, e))
|
logger.error("Error _create_host, code was %s error was %s" % (e.code, e))
|
||||||
return e.code
|
raise e
|
||||||
|
|
||||||
def _convert_list_to_dict(self, listToConvert: list[tuple[str, list]]):
|
def _convert_list_to_dict(self, listToConvert: list[tuple[str, list]]):
|
||||||
"""converts a list of hosts into a dictionary
|
"""converts a list of hosts into a dictionary
|
||||||
|
@ -293,14 +296,16 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
newDict[tup[0]] = tup[1]
|
newDict[tup[0]] = tup[1]
|
||||||
return newDict
|
return newDict
|
||||||
|
|
||||||
def isSubdomain(self, nameserver: str):
|
@classmethod
|
||||||
|
def isSubdomain(cls, name: str, nameserver: str):
|
||||||
"""Returns boolean if the domain name is found in the argument passed"""
|
"""Returns boolean if the domain name is found in the argument passed"""
|
||||||
subdomain_pattern = r"([\w-]+\.)*"
|
subdomain_pattern = r"([\w-]+\.)*"
|
||||||
full_pattern = subdomain_pattern + self.name
|
full_pattern = subdomain_pattern + name
|
||||||
regex = re.compile(full_pattern)
|
regex = re.compile(full_pattern)
|
||||||
return bool(regex.match(nameserver))
|
return bool(regex.match(nameserver))
|
||||||
|
|
||||||
def checkHostIPCombo(self, nameserver: str, ip: list[str]):
|
@classmethod
|
||||||
|
def checkHostIPCombo(cls, name: str, nameserver: str, ip: list[str]):
|
||||||
"""Checks the parameters past for a valid combination
|
"""Checks the parameters past for a valid combination
|
||||||
raises error if:
|
raises error if:
|
||||||
- nameserver is a subdomain but is missing ip
|
- nameserver is a subdomain but is missing ip
|
||||||
|
@ -314,22 +319,23 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
NameserverError (if exception hit)
|
NameserverError (if exception hit)
|
||||||
Returns:
|
Returns:
|
||||||
None"""
|
None"""
|
||||||
if self.isSubdomain(nameserver) and (ip is None or ip == []):
|
if cls.isSubdomain(name, nameserver) and (ip is None or ip == []):
|
||||||
raise NameserverError(code=nsErrorCodes.MISSING_IP, nameserver=nameserver)
|
raise NameserverError(code=nsErrorCodes.MISSING_IP, nameserver=nameserver)
|
||||||
|
|
||||||
elif not self.isSubdomain(nameserver) and (ip is not None and ip != []):
|
elif not cls.isSubdomain(name, nameserver) and (ip is not None and ip != []):
|
||||||
raise NameserverError(
|
raise NameserverError(
|
||||||
code=nsErrorCodes.GLUE_RECORD_NOT_ALLOWED, nameserver=nameserver, ip=ip
|
code=nsErrorCodes.GLUE_RECORD_NOT_ALLOWED, nameserver=nameserver, ip=ip
|
||||||
)
|
)
|
||||||
elif ip is not None and ip != []:
|
elif ip is not None and ip != []:
|
||||||
for addr in ip:
|
for addr in ip:
|
||||||
if not self._valid_ip_addr(addr):
|
if not cls._valid_ip_addr(addr):
|
||||||
raise NameserverError(
|
raise NameserverError(
|
||||||
code=nsErrorCodes.INVALID_IP, nameserver=nameserver, ip=ip
|
code=nsErrorCodes.INVALID_IP, nameserver=nameserver, ip=ip
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _valid_ip_addr(self, ipToTest: str):
|
@classmethod
|
||||||
|
def _valid_ip_addr(cls, ipToTest: str):
|
||||||
"""returns boolean if valid ip address string
|
"""returns boolean if valid ip address string
|
||||||
We currently only accept v4 or v6 ips
|
We currently only accept v4 or v6 ips
|
||||||
returns:
|
returns:
|
||||||
|
@ -382,7 +388,9 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
if newHostDict[prevHost] is not None and set(
|
if newHostDict[prevHost] is not None and set(
|
||||||
newHostDict[prevHost]
|
newHostDict[prevHost]
|
||||||
) != set(addrs):
|
) != set(addrs):
|
||||||
self.checkHostIPCombo(nameserver=prevHost, ip=newHostDict[prevHost])
|
self.__class__.checkHostIPCombo(
|
||||||
|
name=self.name, nameserver=prevHost, ip=newHostDict[prevHost]
|
||||||
|
)
|
||||||
updated_values.append((prevHost, newHostDict[prevHost]))
|
updated_values.append((prevHost, newHostDict[prevHost]))
|
||||||
|
|
||||||
new_values = {
|
new_values = {
|
||||||
|
@ -392,7 +400,9 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
}
|
}
|
||||||
|
|
||||||
for nameserver, ip in new_values.items():
|
for nameserver, ip in new_values.items():
|
||||||
self.checkHostIPCombo(nameserver=nameserver, ip=ip)
|
self.__class__.checkHostIPCombo(
|
||||||
|
name=self.name, nameserver=nameserver, ip=ip
|
||||||
|
)
|
||||||
|
|
||||||
return (deleted_values, updated_values, new_values, previousHostDict)
|
return (deleted_values, updated_values, new_values, previousHostDict)
|
||||||
|
|
||||||
|
@ -567,7 +577,11 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
if len(hosts) > 13:
|
if len(hosts) > 13:
|
||||||
raise NameserverError(code=nsErrorCodes.TOO_MANY_HOSTS)
|
raise NameserverError(code=nsErrorCodes.TOO_MANY_HOSTS)
|
||||||
|
|
||||||
if self.state not in [self.State.DNS_NEEDED, self.State.READY]:
|
if self.state not in [
|
||||||
|
self.State.DNS_NEEDED,
|
||||||
|
self.State.READY,
|
||||||
|
self.State.UNKNOWN,
|
||||||
|
]:
|
||||||
raise ActionNotAllowed("Nameservers can not be " "set in the current state")
|
raise ActionNotAllowed("Nameservers can not be " "set in the current state")
|
||||||
|
|
||||||
logger.info("Setting nameservers")
|
logger.info("Setting nameservers")
|
||||||
|
@ -1351,7 +1365,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
|
|
||||||
@transition(
|
@transition(
|
||||||
field="state",
|
field="state",
|
||||||
source=[State.DNS_NEEDED],
|
source=[State.DNS_NEEDED, State.READY],
|
||||||
target=State.READY,
|
target=State.READY,
|
||||||
# conditions=[dns_not_needed]
|
# conditions=[dns_not_needed]
|
||||||
)
|
)
|
||||||
|
@ -1514,7 +1528,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
data = registry.send(req, cleaned=True).res_data[0]
|
data = registry.send(req, cleaned=True).res_data[0]
|
||||||
host = {
|
host = {
|
||||||
"name": name,
|
"name": name,
|
||||||
"addrs": getattr(data, "addrs", ...),
|
"addrs": [item.addr for item in getattr(data, "addrs", [])],
|
||||||
"cr_date": getattr(data, "cr_date", ...),
|
"cr_date": getattr(data, "cr_date", ...),
|
||||||
"statuses": getattr(data, "statuses", ...),
|
"statuses": getattr(data, "statuses", ...),
|
||||||
"tr_date": getattr(data, "tr_date", ...),
|
"tr_date": getattr(data, "tr_date", ...),
|
||||||
|
@ -1539,10 +1553,9 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
for ip_addr in ip_list:
|
for ip_addr in ip_list:
|
||||||
if self.is_ipv6(ip_addr):
|
edited_ip_list.append(
|
||||||
edited_ip_list.append(epp.Ip(addr=ip_addr, ip="v6"))
|
epp.Ip(addr=ip_addr, ip="v6" if self.is_ipv6(ip_addr) else None)
|
||||||
else: # default ip addr is v4
|
)
|
||||||
edited_ip_list.append(epp.Ip(addr=ip_addr))
|
|
||||||
|
|
||||||
return edited_ip_list
|
return edited_ip_list
|
||||||
|
|
||||||
|
@ -1580,7 +1593,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
return response.code
|
return response.code
|
||||||
except RegistryError as e:
|
except RegistryError as e:
|
||||||
logger.error("Error _update_host, code was %s error was %s" % (e.code, e))
|
logger.error("Error _update_host, code was %s error was %s" % (e.code, e))
|
||||||
return e.code
|
raise e
|
||||||
|
|
||||||
def addAndRemoveHostsFromDomain(
|
def addAndRemoveHostsFromDomain(
|
||||||
self, hostsToAdd: list[str], hostsToDelete: list[str]
|
self, hostsToAdd: list[str], hostsToDelete: list[str]
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
{{ formset.management_form }}
|
{{ formset.management_form }}
|
||||||
|
|
||||||
{% for form in formset %}
|
{% for form in formset %}
|
||||||
<fieldset class="ds-record">
|
<fieldset class="repeatable-form">
|
||||||
|
|
||||||
<legend class="sr-only">DS Data record {{forloop.counter}}</legend>
|
<legend class="sr-only">DS Data record {{forloop.counter}}</legend>
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<button type="button" class="usa-button usa-button--unstyled display-block margin-bottom-2" id="add-ds-form">
|
<button type="button" class="usa-button usa-button--unstyled display-block margin-bottom-2" id="add-form">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||||
</svg><span class="margin-left-05">Add new record</span>
|
</svg><span class="margin-left-05">Add new record</span>
|
||||||
|
|
|
@ -11,40 +11,79 @@
|
||||||
|
|
||||||
<h1>DNS name servers</h1>
|
<h1>DNS name servers</h1>
|
||||||
|
|
||||||
<p>Before your domain can be used we'll need information about your domain
|
<p>Before your domain can be used we’ll need information about your domain name servers. Name server records indicate which DNS server is authoritative for your domain.</p>
|
||||||
name servers.</p>
|
|
||||||
|
<p>Add a name server record by entering the address (e.g., ns1.nameserver.com) in the name server fields below. You must add at least two name servers (13 max).</p>
|
||||||
|
|
||||||
|
<div class="usa-alert usa-alert--slim usa-alert--info">
|
||||||
|
<div class="usa-alert__body">
|
||||||
|
<p class="margin-top-0">Add an IP address only when your name server's address includes your domain name (e.g., if your domain name is "example.gov" and your name server is "ns1.example.gov,” then an IP address is required.) To add multiple IP addresses, separate them with commas.</p>
|
||||||
|
<p class="margin-bottom-0">This step is uncommon unless you self-host your DNS or use custom addresses for your nameserver.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% include "includes/required_fields.html" %}
|
{% include "includes/required_fields.html" %}
|
||||||
|
|
||||||
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
<form class="usa-form usa-form--extra-large" method="post" novalidate id="form-container">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ formset.management_form }}
|
{{ formset.management_form }}
|
||||||
|
|
||||||
{% for form in formset %}
|
{% for form in formset %}
|
||||||
<div class="server-form">
|
<div class="repeatable-form">
|
||||||
{% with sublabel_text="Example: ns"|concat:forloop.counter|concat:".example.com" %}
|
<div class="grid-row grid-gap-2 flex-end">
|
||||||
{% if forloop.counter <= 2 %}
|
<div class="tablet:grid-col-5">
|
||||||
{% with attr_required=True %}
|
{{ form.domain }}
|
||||||
{% input_with_errors form.server %}
|
{% with sublabel_text="Example: ns"|concat:forloop.counter|concat:".example.com" %}
|
||||||
{% endwith %}
|
{% if forloop.counter <= 2 %}
|
||||||
{% else %}
|
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
|
||||||
{% input_with_errors form.server %}
|
{% input_with_errors form.server %}
|
||||||
{% endif %}
|
{% endwith %}
|
||||||
{% endwith %}
|
{% else %}
|
||||||
|
{% input_with_errors form.server %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
<div class="tablet:grid-col-5">
|
||||||
|
{% with sublabel_text="Example: 86.124.49.54 or 2001:db8::1234:5678" add_group_class="usa-form-group--unstyled-error" %}
|
||||||
|
{% input_with_errors form.ip %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
<div class="tablet:grid-col-2">
|
||||||
|
{% comment %} TODO: remove this if for 1192 {% endcomment %}
|
||||||
|
{% if forloop.counter > 2 %}
|
||||||
|
<button type="button" class="usa-button usa-button--unstyled display-block delete-record margin-bottom-075">
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||||
|
</svg><span class="margin-left-05">Delete</span>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<button type="button" class="usa-button usa-button--unstyled display-block" id="add-nameserver-form">
|
<button type="button" class="usa-button usa-button--unstyled display-block" id="add-form">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||||
</svg><span class="margin-left-05">Add another name server</span>
|
</svg><span class="margin-left-05">Add another name server</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
{% comment %} Work around USWDS' button margins to add some spacing between the submit and the 'add more'
|
||||||
type="submit"
|
This solution still works when we remove the 'add more' at 13 forms {% endcomment %}
|
||||||
class="usa-button"
|
<div class="margin-top-2">
|
||||||
>Save
|
<button
|
||||||
</button>
|
type="submit"
|
||||||
</form>
|
class="usa-button"
|
||||||
|
>Save
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="usa-button usa-button--outline"
|
||||||
|
name="btn-cancel-click"
|
||||||
|
aria-label="Reset the data in the Name Server form to the registry state (undo changes)"
|
||||||
|
>Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
{% endblock %} {# domain_content #}
|
{% endblock %} {# domain_content #}
|
||||||
|
|
|
@ -756,7 +756,7 @@ class MockEppLib(TestCase):
|
||||||
mockDataInfoHosts = fakedEppObject(
|
mockDataInfoHosts = fakedEppObject(
|
||||||
"lastPw",
|
"lastPw",
|
||||||
cr_date=datetime.datetime(2023, 8, 25, 19, 45, 35),
|
cr_date=datetime.datetime(2023, 8, 25, 19, 45, 35),
|
||||||
addrs=["1.2.3.4", "2.3.4.5"],
|
addrs=[common.Ip(addr="1.2.3.4"), common.Ip(addr="2.3.4.5")],
|
||||||
)
|
)
|
||||||
|
|
||||||
mockDataHostChange = fakedEppObject(
|
mockDataHostChange = fakedEppObject(
|
||||||
|
@ -813,7 +813,7 @@ class MockEppLib(TestCase):
|
||||||
"ns2.nameserverwithip.gov",
|
"ns2.nameserverwithip.gov",
|
||||||
"ns3.nameserverwithip.gov",
|
"ns3.nameserverwithip.gov",
|
||||||
],
|
],
|
||||||
addrs=["1.2.3.4", "2.3.4.5"],
|
addrs=[common.Ip(addr="1.2.3.4"), common.Ip(addr="2.3.4.5")],
|
||||||
)
|
)
|
||||||
|
|
||||||
justNameserver = fakedEppObject(
|
justNameserver = fakedEppObject(
|
||||||
|
|
|
@ -107,7 +107,7 @@ class TestDomainCache(MockEppLib):
|
||||||
}
|
}
|
||||||
expectedHostsDict = {
|
expectedHostsDict = {
|
||||||
"name": self.mockDataInfoDomain.hosts[0],
|
"name": self.mockDataInfoDomain.hosts[0],
|
||||||
"addrs": self.mockDataInfoHosts.addrs,
|
"addrs": [item.addr for item in self.mockDataInfoHosts.addrs],
|
||||||
"cr_date": self.mockDataInfoHosts.cr_date,
|
"cr_date": self.mockDataInfoHosts.cr_date,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,10 +10,7 @@ class TestNameserverError(TestCase):
|
||||||
def test_with_no_ip(self):
|
def test_with_no_ip(self):
|
||||||
"""Test NameserverError when no ip address is passed"""
|
"""Test NameserverError when no ip address is passed"""
|
||||||
nameserver = "nameserver val"
|
nameserver = "nameserver val"
|
||||||
expected = (
|
expected = "Using your domain for a name server requires an IP address"
|
||||||
f"Nameserver {nameserver} needs to have an "
|
|
||||||
"IP address because it is a subdomain"
|
|
||||||
)
|
|
||||||
|
|
||||||
nsException = NameserverError(
|
nsException = NameserverError(
|
||||||
code=nsErrorCodes.MISSING_IP, nameserver=nameserver
|
code=nsErrorCodes.MISSING_IP, nameserver=nameserver
|
||||||
|
@ -38,7 +35,7 @@ class TestNameserverError(TestCase):
|
||||||
ip = "ip val"
|
ip = "ip val"
|
||||||
nameserver = "nameserver val"
|
nameserver = "nameserver val"
|
||||||
|
|
||||||
expected = f"Nameserver {nameserver} has an invalid IP address: {ip}"
|
expected = f"{nameserver}: Enter an IP address in the required format."
|
||||||
nsException = NameserverError(
|
nsException = NameserverError(
|
||||||
code=nsErrorCodes.INVALID_IP, nameserver=nameserver, ip=ip
|
code=nsErrorCodes.INVALID_IP, nameserver=nameserver, ip=ip
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,6 +10,10 @@ from .common import MockEppLib, completed_application # type: ignore
|
||||||
from django_webtest import WebTest # type: ignore
|
from django_webtest import WebTest # type: ignore
|
||||||
import boto3_mocking # type: ignore
|
import boto3_mocking # type: ignore
|
||||||
|
|
||||||
|
from registrar.utility.errors import (
|
||||||
|
NameserverError,
|
||||||
|
NameserverErrorCodes,
|
||||||
|
)
|
||||||
|
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
DomainApplication,
|
DomainApplication,
|
||||||
|
@ -1442,20 +1446,165 @@ class TestDomainNameservers(TestDomainOverview):
|
||||||
)
|
)
|
||||||
self.assertContains(page, "DNS name servers")
|
self.assertContains(page, "DNS name servers")
|
||||||
|
|
||||||
@skip("Broken by adding registry connection fix in ticket 848")
|
def test_domain_nameservers_form_submit_one_nameserver(self):
|
||||||
def test_domain_nameservers_form(self):
|
"""Nameserver form submitted with one nameserver throws error.
|
||||||
"""Can change domain's nameservers.
|
|
||||||
|
|
||||||
Uses self.app WebTest because we need to interact with forms.
|
Uses self.app WebTest because we need to interact with forms.
|
||||||
"""
|
"""
|
||||||
|
# initial nameservers page has one server with two ips
|
||||||
nameservers_page = self.app.get(
|
nameservers_page = self.app.get(
|
||||||
reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})
|
reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})
|
||||||
)
|
)
|
||||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
# attempt to submit the form with only one nameserver, should error
|
||||||
|
# regarding required fields
|
||||||
with less_console_noise(): # swallow log warning message
|
with less_console_noise(): # swallow log warning message
|
||||||
result = nameservers_page.form.submit()
|
result = nameservers_page.form.submit()
|
||||||
# form submission was a post, response should be a redirect
|
# 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 required field. form requires a minimum of 2 name servers
|
||||||
|
self.assertContains(result, "This field is required.", count=2, status_code=200)
|
||||||
|
|
||||||
|
def test_domain_nameservers_form_submit_subdomain_missing_ip(self):
|
||||||
|
"""Nameserver form catches missing ip error on subdomain.
|
||||||
|
|
||||||
|
Uses self.app WebTest because we need to interact with forms.
|
||||||
|
"""
|
||||||
|
# initial nameservers page has one server with two ips
|
||||||
|
nameservers_page = self.app.get(
|
||||||
|
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)
|
||||||
|
# attempt to submit the form without two hosts, both subdomains,
|
||||||
|
# only one has ips
|
||||||
|
nameservers_page.form["form-1-server"] = "ns2.igorville.gov"
|
||||||
|
with less_console_noise(): # swallow log warning message
|
||||||
|
result = nameservers_page.form.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 required field. subdomain missing an ip
|
||||||
|
self.assertContains(
|
||||||
|
result,
|
||||||
|
str(NameserverError(code=NameserverErrorCodes.MISSING_IP)),
|
||||||
|
count=2,
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_domain_nameservers_form_submit_missing_host(self):
|
||||||
|
"""Nameserver form catches error when host is missing.
|
||||||
|
|
||||||
|
Uses self.app WebTest because we need to interact with forms.
|
||||||
|
"""
|
||||||
|
# initial nameservers page has one server with two ips
|
||||||
|
nameservers_page = self.app.get(
|
||||||
|
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)
|
||||||
|
# attempt to submit the form without two hosts, both subdomains,
|
||||||
|
# only one has ips
|
||||||
|
nameservers_page.form["form-1-ip"] = "127.0.0.1"
|
||||||
|
with less_console_noise(): # swallow log warning message
|
||||||
|
result = nameservers_page.form.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 required field. nameserver has ip but missing host
|
||||||
|
self.assertContains(
|
||||||
|
result,
|
||||||
|
str(NameserverError(code=NameserverErrorCodes.MISSING_HOST)),
|
||||||
|
count=2,
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_domain_nameservers_form_submit_glue_record_not_allowed(self):
|
||||||
|
"""Nameserver form catches error when IP is present
|
||||||
|
but host not subdomain.
|
||||||
|
|
||||||
|
Uses self.app WebTest because we need to interact with forms.
|
||||||
|
"""
|
||||||
|
nameserver1 = "ns1.igorville.gov"
|
||||||
|
nameserver2 = "ns2.igorville.com"
|
||||||
|
valid_ip = "127.0.0.1"
|
||||||
|
# initial nameservers page has one server with two ips
|
||||||
|
nameservers_page = self.app.get(
|
||||||
|
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)
|
||||||
|
# attempt to submit the form without two hosts, both subdomains,
|
||||||
|
# only one has ips
|
||||||
|
nameservers_page.form["form-0-server"] = nameserver1
|
||||||
|
nameservers_page.form["form-1-server"] = nameserver2
|
||||||
|
nameservers_page.form["form-1-ip"] = valid_ip
|
||||||
|
with less_console_noise(): # swallow log warning message
|
||||||
|
result = nameservers_page.form.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 required field. nameserver has ip but missing host
|
||||||
|
self.assertContains(
|
||||||
|
result,
|
||||||
|
str(NameserverError(code=NameserverErrorCodes.GLUE_RECORD_NOT_ALLOWED)),
|
||||||
|
count=2,
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_domain_nameservers_form_submit_invalid_ip(self):
|
||||||
|
"""Nameserver form catches invalid IP on submission.
|
||||||
|
|
||||||
|
Uses self.app WebTest because we need to interact with forms.
|
||||||
|
"""
|
||||||
|
nameserver = "ns2.igorville.gov"
|
||||||
|
invalid_ip = "123"
|
||||||
|
# initial nameservers page has one server with two ips
|
||||||
|
nameservers_page = self.app.get(
|
||||||
|
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)
|
||||||
|
# attempt to submit the form without two hosts, both subdomains,
|
||||||
|
# only one has ips
|
||||||
|
nameservers_page.form["form-1-server"] = nameserver
|
||||||
|
nameservers_page.form["form-1-ip"] = invalid_ip
|
||||||
|
with less_console_noise(): # swallow log warning message
|
||||||
|
result = nameservers_page.form.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 required field. nameserver has ip but missing host
|
||||||
|
self.assertContains(
|
||||||
|
result,
|
||||||
|
str(
|
||||||
|
NameserverError(
|
||||||
|
code=NameserverErrorCodes.INVALID_IP, nameserver=nameserver
|
||||||
|
)
|
||||||
|
),
|
||||||
|
count=2,
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_domain_nameservers_form_submits_successfully(self):
|
||||||
|
"""Nameserver form submits successfully with valid input.
|
||||||
|
|
||||||
|
Uses self.app WebTest because we need to interact with forms.
|
||||||
|
"""
|
||||||
|
nameserver1 = "ns1.igorville.gov"
|
||||||
|
nameserver2 = "ns2.igorville.gov"
|
||||||
|
invalid_ip = "127.0.0.1"
|
||||||
|
# initial nameservers page has one server with two ips
|
||||||
|
nameservers_page = self.app.get(
|
||||||
|
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)
|
||||||
|
# attempt to submit the form without two hosts, both subdomains,
|
||||||
|
# only one has ips
|
||||||
|
nameservers_page.form["form-0-server"] = nameserver1
|
||||||
|
nameservers_page.form["form-1-server"] = nameserver2
|
||||||
|
nameservers_page.form["form-1-ip"] = invalid_ip
|
||||||
|
with less_console_noise(): # swallow log warning message
|
||||||
|
result = nameservers_page.form.submit()
|
||||||
|
# form submission was a successful post, response should be a 302
|
||||||
self.assertEqual(result.status_code, 302)
|
self.assertEqual(result.status_code, 302)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
result["Location"],
|
result["Location"],
|
||||||
|
@ -1465,9 +1614,8 @@ class TestDomainNameservers(TestDomainOverview):
|
||||||
page = result.follow()
|
page = result.follow()
|
||||||
self.assertContains(page, "The name servers for this domain have been updated")
|
self.assertContains(page, "The name servers for this domain have been updated")
|
||||||
|
|
||||||
@skip("Broken by adding registry connection fix in ticket 848")
|
|
||||||
def test_domain_nameservers_form_invalid(self):
|
def test_domain_nameservers_form_invalid(self):
|
||||||
"""Can change domain's nameservers.
|
"""Nameserver form does not submit with invalid data.
|
||||||
|
|
||||||
Uses self.app WebTest because we need to interact with forms.
|
Uses self.app WebTest because we need to interact with forms.
|
||||||
"""
|
"""
|
||||||
|
@ -1482,9 +1630,9 @@ class TestDomainNameservers(TestDomainOverview):
|
||||||
with less_console_noise(): # swallow logged warning message
|
with less_console_noise(): # swallow logged warning message
|
||||||
result = nameservers_page.form.submit()
|
result = nameservers_page.form.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 four times, twice at the top of the page,
|
||||||
# the field.
|
# once around each required field.
|
||||||
self.assertContains(result, "This field is required", count=2, status_code=200)
|
self.assertContains(result, "This field is required", count=4, status_code=200)
|
||||||
|
|
||||||
|
|
||||||
class TestDomainAuthorizingOfficial(TestDomainOverview):
|
class TestDomainAuthorizingOfficial(TestDomainOverview):
|
||||||
|
|
|
@ -20,6 +20,41 @@ class ActionNotAllowed(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GenericErrorCodes(IntEnum):
|
||||||
|
"""Used across the registrar for
|
||||||
|
error mapping.
|
||||||
|
Overview of generic error codes:
|
||||||
|
- 1 GENERIC_ERROR a generic value error
|
||||||
|
- 2 CANNOT_CONTACT_REGISTRY a connection error w registry
|
||||||
|
"""
|
||||||
|
|
||||||
|
GENERIC_ERROR = 1
|
||||||
|
CANNOT_CONTACT_REGISTRY = 2
|
||||||
|
|
||||||
|
|
||||||
|
class GenericError(Exception):
|
||||||
|
"""
|
||||||
|
GenericError class used to raise exceptions across
|
||||||
|
the registrar
|
||||||
|
"""
|
||||||
|
|
||||||
|
_error_mapping = {
|
||||||
|
GenericErrorCodes.CANNOT_CONTACT_REGISTRY: (
|
||||||
|
"Update failed. Cannot contact the registry."
|
||||||
|
),
|
||||||
|
GenericErrorCodes.GENERIC_ERROR: ("Value entered was wrong."),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, code=None, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.code = code
|
||||||
|
if self.code in self._error_mapping:
|
||||||
|
self.message = self._error_mapping.get(self.code)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.message}"
|
||||||
|
|
||||||
|
|
||||||
class NameserverErrorCodes(IntEnum):
|
class NameserverErrorCodes(IntEnum):
|
||||||
"""Used in the NameserverError class for
|
"""Used in the NameserverError class for
|
||||||
error mapping.
|
error mapping.
|
||||||
|
@ -29,6 +64,8 @@ class NameserverErrorCodes(IntEnum):
|
||||||
value but is not a subdomain
|
value but is not a subdomain
|
||||||
- 3 INVALID_IP invalid ip address format or invalid version
|
- 3 INVALID_IP invalid ip address format or invalid version
|
||||||
- 4 TOO_MANY_HOSTS more than the max allowed host values
|
- 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
|
||||||
"""
|
"""
|
||||||
|
|
||||||
MISSING_IP = 1
|
MISSING_IP = 1
|
||||||
|
@ -36,6 +73,7 @@ class NameserverErrorCodes(IntEnum):
|
||||||
INVALID_IP = 3
|
INVALID_IP = 3
|
||||||
TOO_MANY_HOSTS = 4
|
TOO_MANY_HOSTS = 4
|
||||||
UNABLE_TO_UPDATE_DOMAIN = 5
|
UNABLE_TO_UPDATE_DOMAIN = 5
|
||||||
|
MISSING_HOST = 6
|
||||||
|
|
||||||
|
|
||||||
class NameserverError(Exception):
|
class NameserverError(Exception):
|
||||||
|
@ -45,11 +83,15 @@ class NameserverError(Exception):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_error_mapping = {
|
_error_mapping = {
|
||||||
NameserverErrorCodes.MISSING_IP: "Nameserver {} needs to have an "
|
NameserverErrorCodes.MISSING_IP: (
|
||||||
"IP address because it is a subdomain",
|
"Using your domain for a name server requires an IP address"
|
||||||
NameserverErrorCodes.GLUE_RECORD_NOT_ALLOWED: "Nameserver {} cannot be linked "
|
),
|
||||||
"because it is not a subdomain",
|
NameserverErrorCodes.GLUE_RECORD_NOT_ALLOWED: (
|
||||||
NameserverErrorCodes.INVALID_IP: "Nameserver {} has an invalid IP address: {}",
|
"Name server address does not match domain name"
|
||||||
|
),
|
||||||
|
NameserverErrorCodes.INVALID_IP: (
|
||||||
|
"{}: Enter an IP address in the required format."
|
||||||
|
),
|
||||||
NameserverErrorCodes.TOO_MANY_HOSTS: (
|
NameserverErrorCodes.TOO_MANY_HOSTS: (
|
||||||
"Too many hosts provided, you may not have more than 13 nameservers."
|
"Too many hosts provided, you may not have more than 13 nameservers."
|
||||||
),
|
),
|
||||||
|
@ -57,6 +99,9 @@ class NameserverError(Exception):
|
||||||
"Unable to update domain, changes were not applied."
|
"Unable to update domain, changes were not applied."
|
||||||
"Check logs as a Registry Error is the likely cause"
|
"Check logs as a Registry Error is the likely cause"
|
||||||
),
|
),
|
||||||
|
NameserverErrorCodes.MISSING_HOST: (
|
||||||
|
"Name server must be provided to enter IP address."
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, code=None, nameserver=None, ip=None, **kwargs):
|
def __init__(self, *args, code=None, nameserver=None, ip=None, **kwargs):
|
||||||
|
@ -65,7 +110,7 @@ class NameserverError(Exception):
|
||||||
if self.code in self._error_mapping:
|
if self.code in self._error_mapping:
|
||||||
self.message = self._error_mapping.get(self.code)
|
self.message = self._error_mapping.get(self.code)
|
||||||
if nameserver is not None and ip is not None:
|
if nameserver is not None and ip is not None:
|
||||||
self.message = self.message.format(str(nameserver), str(ip))
|
self.message = self.message.format(str(nameserver))
|
||||||
elif nameserver is not None:
|
elif nameserver is not None:
|
||||||
self.message = self.message.format(str(nameserver))
|
self.message = self.message.format(str(nameserver))
|
||||||
elif ip is not None:
|
elif ip is not None:
|
||||||
|
|
|
@ -23,6 +23,12 @@ from registrar.models import (
|
||||||
UserDomainRole,
|
UserDomainRole,
|
||||||
)
|
)
|
||||||
from registrar.models.public_contact import PublicContact
|
from registrar.models.public_contact import PublicContact
|
||||||
|
from registrar.utility.errors import (
|
||||||
|
GenericError,
|
||||||
|
GenericErrorCodes,
|
||||||
|
NameserverError,
|
||||||
|
NameserverErrorCodes as nsErrorCodes,
|
||||||
|
)
|
||||||
from registrar.models.utility.contact_error import ContactError
|
from registrar.models.utility.contact_error import ContactError
|
||||||
|
|
||||||
from ..forms import (
|
from ..forms import (
|
||||||
|
@ -40,8 +46,6 @@ from epplibwrapper import (
|
||||||
common,
|
common,
|
||||||
extensions,
|
extensions,
|
||||||
RegistryError,
|
RegistryError,
|
||||||
CANNOT_CONTACT_REGISTRY,
|
|
||||||
GENERIC_ERROR,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..utility.email import send_templated_email, EmailSendingError
|
from ..utility.email import send_templated_email, EmailSendingError
|
||||||
|
@ -215,6 +219,7 @@ class DomainNameserversView(DomainFormBaseView):
|
||||||
|
|
||||||
template_name = "domain_nameservers.html"
|
template_name = "domain_nameservers.html"
|
||||||
form_class = NameserverFormset
|
form_class = NameserverFormset
|
||||||
|
model = Domain
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
"""The initial value for the form (which is a formset here)."""
|
"""The initial value for the form (which is a formset here)."""
|
||||||
|
@ -223,7 +228,9 @@ class DomainNameserversView(DomainFormBaseView):
|
||||||
|
|
||||||
if nameservers is not None:
|
if nameservers is not None:
|
||||||
# Add existing nameservers as initial data
|
# Add existing nameservers as initial data
|
||||||
initial_data.extend({"server": name} for name, *ip in nameservers)
|
initial_data.extend(
|
||||||
|
{"server": name, "ip": ",".join(ip)} for name, ip in nameservers
|
||||||
|
)
|
||||||
|
|
||||||
# Ensure at least 3 fields, filled or empty
|
# Ensure at least 3 fields, filled or empty
|
||||||
while len(initial_data) < 2:
|
while len(initial_data) < 2:
|
||||||
|
@ -252,25 +259,82 @@ class DomainNameserversView(DomainFormBaseView):
|
||||||
form.fields["server"].required = True
|
form.fields["server"].required = True
|
||||||
else:
|
else:
|
||||||
form.fields["server"].required = False
|
form.fields["server"].required = False
|
||||||
|
form.fields["domain"].initial = self.object.name
|
||||||
return formset
|
return formset
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
"""Form submission posts to this view.
|
||||||
|
|
||||||
|
This post method harmonizes using DomainBaseView and FormMixin
|
||||||
|
"""
|
||||||
|
self._get_domain(request)
|
||||||
|
formset = self.get_form()
|
||||||
|
|
||||||
|
if "btn-cancel-click" in request.POST:
|
||||||
|
url = self.get_success_url()
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
if formset.is_valid():
|
||||||
|
return self.form_valid(formset)
|
||||||
|
else:
|
||||||
|
return self.form_invalid(formset)
|
||||||
|
|
||||||
def form_valid(self, formset):
|
def form_valid(self, formset):
|
||||||
"""The formset is valid, perform something with it."""
|
"""The formset is valid, perform something with it."""
|
||||||
|
|
||||||
|
self.request.session["nameservers_form_domain"] = self.object
|
||||||
|
|
||||||
# Set the nameservers from the formset
|
# Set the nameservers from the formset
|
||||||
nameservers = []
|
nameservers = []
|
||||||
for form in formset:
|
for form in formset:
|
||||||
try:
|
try:
|
||||||
as_tuple = (form.cleaned_data["server"],)
|
ip_string = form.cleaned_data["ip"]
|
||||||
|
# ip_string will be None or a string of IP addresses
|
||||||
|
# comma-separated
|
||||||
|
ip_list = []
|
||||||
|
if ip_string:
|
||||||
|
# Split the string into a list using a comma as the delimiter
|
||||||
|
ip_list = ip_string.split(",")
|
||||||
|
|
||||||
|
as_tuple = (
|
||||||
|
form.cleaned_data["server"],
|
||||||
|
ip_list,
|
||||||
|
)
|
||||||
nameservers.append(as_tuple)
|
nameservers.append(as_tuple)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# no server information in this field, skip it
|
# no server information in this field, skip it
|
||||||
pass
|
pass
|
||||||
self.object.nameservers = nameservers
|
|
||||||
|
|
||||||
messages.success(
|
try:
|
||||||
self.request, "The name servers for this domain have been updated."
|
self.object.nameservers = nameservers
|
||||||
)
|
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)
|
||||||
|
)
|
||||||
|
logger.error(f"Nameservers error: {Err}")
|
||||||
|
# TODO: registry is not throwing an error when no connection
|
||||||
|
except RegistryError as Err:
|
||||||
|
if Err.is_connection_error():
|
||||||
|
messages.error(
|
||||||
|
self.request,
|
||||||
|
GenericError(code=GenericErrorCodes.CANNOT_CONTACT_REGISTRY),
|
||||||
|
)
|
||||||
|
logger.error(f"Registry connection error: {Err}")
|
||||||
|
else:
|
||||||
|
messages.error(
|
||||||
|
self.request, GenericError(code=GenericErrorCodes.GENERIC_ERROR)
|
||||||
|
)
|
||||||
|
logger.error(f"Registry error: {Err}")
|
||||||
|
else:
|
||||||
|
messages.success(
|
||||||
|
self.request,
|
||||||
|
"The name servers for this domain have been updated. "
|
||||||
|
"Keep in mind that DNS changes may take some time to "
|
||||||
|
"propagate across the internet. It can take anywhere "
|
||||||
|
"from a few minutes to 48 hours for your changes to take place.",
|
||||||
|
)
|
||||||
|
|
||||||
# superclass has the redirect
|
# superclass has the redirect
|
||||||
return super().form_valid(formset)
|
return super().form_valid(formset)
|
||||||
|
@ -431,10 +495,15 @@ class DomainDsDataView(DomainFormBaseView):
|
||||||
self.object.dnssecdata = dnssecdata
|
self.object.dnssecdata = dnssecdata
|
||||||
except RegistryError as err:
|
except RegistryError as err:
|
||||||
if err.is_connection_error():
|
if err.is_connection_error():
|
||||||
messages.error(self.request, CANNOT_CONTACT_REGISTRY)
|
messages.error(
|
||||||
|
self.request,
|
||||||
|
GenericError(code=GenericErrorCodes.CANNOT_CONTACT_REGISTRY),
|
||||||
|
)
|
||||||
logger.error(f"Registry connection error: {err}")
|
logger.error(f"Registry connection error: {err}")
|
||||||
else:
|
else:
|
||||||
messages.error(self.request, GENERIC_ERROR)
|
messages.error(
|
||||||
|
self.request, GenericError(code=GenericErrorCodes.GENERIC_ERROR)
|
||||||
|
)
|
||||||
logger.error(f"Registry error: {err}")
|
logger.error(f"Registry error: {err}")
|
||||||
return self.form_invalid(formset)
|
return self.form_invalid(formset)
|
||||||
else:
|
else:
|
||||||
|
@ -510,7 +579,10 @@ class DomainSecurityEmailView(DomainFormBaseView):
|
||||||
# If no default is created for security_contact,
|
# If no default is created for security_contact,
|
||||||
# then we cannot connect to the registry.
|
# then we cannot connect to the registry.
|
||||||
if contact is None:
|
if contact is None:
|
||||||
messages.error(self.request, CANNOT_CONTACT_REGISTRY)
|
messages.error(
|
||||||
|
self.request,
|
||||||
|
GenericError(code=GenericErrorCodes.CANNOT_CONTACT_REGISTRY),
|
||||||
|
)
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
contact.email = new_email
|
contact.email = new_email
|
||||||
|
@ -519,13 +591,20 @@ class DomainSecurityEmailView(DomainFormBaseView):
|
||||||
contact.save()
|
contact.save()
|
||||||
except RegistryError as Err:
|
except RegistryError as Err:
|
||||||
if Err.is_connection_error():
|
if Err.is_connection_error():
|
||||||
messages.error(self.request, CANNOT_CONTACT_REGISTRY)
|
messages.error(
|
||||||
|
self.request,
|
||||||
|
GenericError(code=GenericErrorCodes.CANNOT_CONTACT_REGISTRY),
|
||||||
|
)
|
||||||
logger.error(f"Registry connection error: {Err}")
|
logger.error(f"Registry connection error: {Err}")
|
||||||
else:
|
else:
|
||||||
messages.error(self.request, GENERIC_ERROR)
|
messages.error(
|
||||||
|
self.request, GenericError(code=GenericErrorCodes.GENERIC_ERROR)
|
||||||
|
)
|
||||||
logger.error(f"Registry error: {Err}")
|
logger.error(f"Registry error: {Err}")
|
||||||
except ContactError as Err:
|
except ContactError as Err:
|
||||||
messages.error(self.request, GENERIC_ERROR)
|
messages.error(
|
||||||
|
self.request, GenericError(code=GenericErrorCodes.GENERIC_ERROR)
|
||||||
|
)
|
||||||
logger.error(f"Generic registry error: {Err}")
|
logger.error(f"Generic registry error: {Err}")
|
||||||
else:
|
else:
|
||||||
messages.success(
|
messages.success(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue