Merge remote-tracking branch 'origin/main' into rjm/1103-security-email-registry-error-handling

This commit is contained in:
Rachid Mrad 2023-10-17 14:27:13 -04:00
commit fa373993bf
No known key found for this signature in database
GPG key ID: EF38E4CEC4A8F3CF
33 changed files with 2319 additions and 710 deletions

View file

@ -236,28 +236,150 @@ function handleValidationClick(e) {
* Only does something on a single page, but it should be fast enough to run
* it everywhere.
*/
(function prepareForms() {
let serverForm = document.querySelectorAll(".server-form")
let container = document.querySelector("#form-container")
let addButton = document.querySelector("#add-form")
let totalForms = document.querySelector("#id_form-TOTAL_FORMS")
(function prepareNameserverForms() {
let serverForm = document.querySelectorAll(".server-form");
let container = document.querySelector("#form-container");
let addButton = document.querySelector("#add-nameserver-form");
let totalForms = document.querySelector("#id_form-TOTAL_FORMS");
let formNum = serverForm.length-1
addButton.addEventListener('click', addForm)
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')
let newForm = serverForm[2].cloneNode(true);
let formNumberRegex = RegExp(`form-(\\d){1}-`,'g');
let formLabelRegex = RegExp(`Name server (\\d){1}`, 'g');
let formExampleRegex = RegExp(`ns(\\d){1}`, 'g');
formNum++
newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum}-`)
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `Name server ${formNum+1}`)
newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum+1}`)
container.insertBefore(newForm, addButton)
newForm.querySelector("input").value = ""
formNum++;
newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum}-`);
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `Name server ${formNum+1}`);
newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum+1}`);
container.insertBefore(newForm, addButton);
newForm.querySelector("input").value = "";
totalForms.setAttribute('value', `${formNum+1}`)
totalForms.setAttribute('value', `${formNum+1}`);
}
})();
function prepareDeleteButtons() {
let deleteButtons = document.querySelectorAll(".delete-record");
let totalForms = document.querySelector("#id_form-TOTAL_FORMS");
// Loop through each delete button and attach the click event listener
deleteButtons.forEach((deleteButton) => {
deleteButton.addEventListener('click', removeForm);
});
function removeForm(e){
let formToRemove = e.target.closest(".ds-record");
formToRemove.remove();
let forms = document.querySelectorAll(".ds-record");
totalForms.setAttribute('value', `${forms.length}`);
let formNumberRegex = RegExp(`form-(\\d){1}-`, 'g');
let formLabelRegex = RegExp(`DS Data record (\\d){1}`, 'g');
forms.forEach((form, index) => {
// Iterate over child nodes of the current element
Array.from(form.querySelectorAll('label, input, select')).forEach((node) => {
// Iterate through the attributes of the current node
Array.from(node.attributes).forEach((attr) => {
// Check if the attribute value matches the regex
if (formNumberRegex.test(attr.value)) {
// Replace the attribute value with the updated value
attr.value = attr.value.replace(formNumberRegex, `form-${index}-`);
}
});
});
Array.from(form.querySelectorAll('h2, legend')).forEach((node) => {
node.textContent = node.textContent.replace(formLabelRegex, `DS Data record ${index + 1}`);
});
});
}
}
/**
* An IIFE that attaches a click handler for our dynamic DNSSEC forms
*
*/
(function prepareDNSSECForms() {
let serverForm = document.querySelectorAll(".ds-record");
let container = document.querySelector("#form-container");
let addButton = document.querySelector("#add-ds-form");
let totalForms = document.querySelector("#id_form-TOTAL_FORMS");
// Attach click event listener on the delete buttons of the existing forms
prepareDeleteButtons();
// Attack click event listener on the add button
if (addButton)
addButton.addEventListener('click', addForm);
/*
* Add a formset to the end of the form.
* For each element in the added formset, name the elements with the prefix,
* form-{#}-{element_name} where # is the index of the formset and element_name
* is the element's name.
* Additionally, update the form element's metadata, including totalForms' value.
*/
function addForm(e){
let forms = document.querySelectorAll(".ds-record");
let formNum = forms.length;
let newForm = serverForm[0].cloneNode(true);
let formNumberRegex = RegExp(`form-(\\d){1}-`,'g');
let formLabelRegex = RegExp(`DS Data record (\\d){1}`, 'g');
formNum++;
newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum-1}-`);
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `DS Data record ${formNum}`);
container.insertBefore(newForm, addButton);
let inputs = newForm.querySelectorAll("input");
// Reset the values of each input to blank
inputs.forEach((input) => {
input.classList.remove("usa-input--error");
if (input.type === "text" || input.type === "number" || input.type === "password") {
input.value = ""; // Set the value to an empty string
} else if (input.type === "checkbox" || input.type === "radio") {
input.checked = false; // Uncheck checkboxes and radios
}
});
// Reset any existing validation classes
let selects = newForm.querySelectorAll("select");
selects.forEach((select) => {
select.classList.remove("usa-input--error");
select.selectedIndex = 0; // Set the value to an empty string
});
let labels = newForm.querySelectorAll("label");
labels.forEach((label) => {
label.classList.remove("usa-label--error");
});
let usaFormGroups = newForm.querySelectorAll(".usa-form-group");
usaFormGroups.forEach((usaFormGroup) => {
usaFormGroup.classList.remove("usa-form-group--error");
});
// Remove any existing error messages
let usaErrorMessages = newForm.querySelectorAll(".usa-error-message");
usaErrorMessages.forEach((usaErrorMessage) => {
let parentDiv = usaErrorMessage.closest('div');
if (parentDiv) {
parentDiv.remove(); // Remove the parent div if it exists
}
});
totalForms.setAttribute('value', `${formNum}`);
// Attach click event listener on the delete buttons of the new form
prepareDeleteButtons();
}
})();

View file

@ -0,0 +1,17 @@
// Fixes some font size disparities with the Figma
// for usa-alert alert elements
.usa-alert {
.usa-alert__heading.larger-font-sizing {
font-size: units(3);
}
}
// The icon was off center for some reason
// Fixes that issue
@media (min-width: 64em){
.usa-alert--warning{
.usa-alert__body::before {
left: 1rem !important;
}
}
}

View file

@ -0,0 +1,127 @@
@use "uswds-core" as *;
/* Styles for making visible to screen reader / AT users only. */
.sr-only {
@include sr-only;
}
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
#wrapper {
flex-grow: 1;
padding-top: units(3);
padding-bottom: units(6) * 2 ; //Workaround because USWDS units jump from 10 to 15
}
#wrapper.dashboard {
background-color: color('primary-lightest');
padding-top: units(5);
}
.usa-logo {
@include at-media(desktop) {
margin-top: units(2);
}
}
.usa-logo__text {
@include typeset('sans', 'xl', 2);
color: color('primary-darker');
}
.usa-nav__primary {
margin-top: units(1);
}
.section--outlined {
background-color: color('white');
border: 1px solid color('base-lighter');
border-radius: 4px;
padding: 0 units(2) units(3);
margin-top: units(3);
h2 {
color: color('primary-dark');
margin-top: units(2);
margin-bottom: units(2);
}
p {
margin-bottom: 0;
}
@include at-media(mobile-lg) {
margin-top: units(5);
h2 {
margin-bottom: 0;
}
}
}
.break-word {
word-break: break-word;
}
.dotgov-status-box {
background-color: color('primary-lightest');
border-color: color('accent-cool-lighter');
}
.dotgov-status-box--action-need {
background-color: color('warning-lighter');
border-color: color('warning');
}
footer {
border-top: 1px solid color('primary-darker');
}
.usa-footer__secondary-section {
background-color: color('primary-lightest');
}
.usa-footer__secondary-section a {
color: color('primary');
}
.usa-identifier__logo {
height: units(7);
}
abbr[title] {
// workaround for underlining abbr element
border-bottom: none;
text-decoration: none;
}
@include at-media(tablet) {
.float-right-tablet {
float: right;
}
.float-left-tablet {
float: left;
}
}
@include at-media(desktop) {
.float-right-desktop {
float: right;
}
.float-left-desktop {
float: left;
}
}
.flex-end {
align-items: flex-end;
}

View file

@ -0,0 +1,125 @@
@use "uswds-core" as *;
/* Make "placeholder" links visually obvious */
a[href$="todo"]::after {
background-color: yellow;
color: color(blue-80v);
content: " [link TBD]";
font-style: italic;
}
a.breadcrumb__back {
display:flex;
align-items: center;
margin-bottom: units(2.5);
&:visited {
color: color('primary');
}
@include at-media('tablet') {
//align to top of sidebar
margin-top: units(-0.5);
}
}
a.usa-button {
text-decoration: none;
color: color('white');
}
a.usa-button:visited,
a.usa-button:hover,
a.usa-button:focus,
a.usa-button:active {
color: color('white');
}
a.usa-button--outline,
a.usa-button--outline:visited {
box-shadow: inset 0 0 0 2px color('primary');
color: color('primary');
}
a.usa-button--outline:hover,
a.usa-button--outline:focus {
box-shadow: inset 0 0 0 2px color('primary-dark');
color: color('primary-dark');
}
a.usa-button--outline:active {
box-shadow: inset 0 0 0 2px color('primary-darker');
color: color('primary-darker');
}
a.withdraw {
background-color: color('error');
}
a.withdraw_outline,
a.withdraw_outline:visited {
box-shadow: inset 0 0 0 2px color('error');
color: color('error');
}
a.withdraw_outline:hover,
a.withdraw_outline:focus {
box-shadow: inset 0 0 0 2px color('error-dark');
color: color('error-dark');
}
a.withdraw_outline:active {
box-shadow: inset 0 0 0 2px color('error-darker');
color: color('error-darker');
}
a.withdraw:hover,
a.withdraw:focus {
background-color: color('error-dark');
}
a.withdraw:active {
background-color: color('error-darker');
}
.usa-button--unstyled .usa-icon {
vertical-align: bottom;
}
a.usa-button--unstyled:visited {
color: color('primary');
}
.dotgov-button--green {
background-color: color('success-dark');
&:hover {
background-color: color('success-darker');
}
&:active {
background-color: color('green-80v');
}
}
// Cancel button used on the
// DNSSEC main page
// We want to center this button on mobile
// and add some extra left margin on tablet+
.usa-button--cancel {
text-align: center;
@include at-media('tablet') {
margin-left: units(2);
}
}
// WARNING: crazy hack ahead:
// Cancel button(s) on the DNSSEC form pages
// We want to position the cancel button on the
// dnssec forms next to the submit button
// This button's markup is in its own form
.btn-cancel {
position: relative;
top: -39.2px;
left: 88px;
}

View file

@ -0,0 +1,10 @@
@use "uswds-core" as *;
fieldset {
border: solid 1px color('base-lighter');
padding: units(3);
}
fieldset:not(:first-child) {
margin-top: units(2);
}

View file

@ -0,0 +1,25 @@
@use "uswds-core" as *;
.usa-form .usa-button {
margin-top: units(3);
}
.usa-form--extra-large {
max-width: none;
}
.usa-form--text-width {
max-width: measure(5);
}
.usa-textarea {
@include at-media('tablet') {
height: units('mobile');
}
}
.usa-form-group--unstyled-error {
margin-left: 0;
padding-left: 0;
border-left: none;
}

View file

@ -0,0 +1,80 @@
@use "uswds-core" as *;
@use "typography" as *;
.register-form-step > h1 {
//align to top of sidebar on first page of the form
margin-top: units(-1);
}
//Tighter spacing when H2 is immediatly after H1
.register-form-step .usa-fieldset:first-of-type h2:first-of-type,
.register-form-step h1 + h2 {
margin-top: units(1);
}
.register-form-step h3 {
color: color('primary-dark');
letter-spacing: $letter-space--xs;
margin-top: units(3);
margin-bottom: 0;
+ p {
margin-top: units(0.5);
}
}
.register-form-step h4 {
margin-bottom: 0;
+ p {
margin-top: units(0.5);
}
}
.register-form-step a {
color: color('primary');
&:visited {
color: color('violet-70v'); //USWDS default
}
}
.register-form-step .usa-form-group:first-of-type,
.register-form-step .usa-label:first-of-type {
margin-top: units(1);
}
.ao_example p {
margin-top: units(1);
}
.domain_example {
p {
margin-bottom: 0;
}
.usa-list {
margin-top: units(0.5);
}
}
.review__step {
margin-top: units(3);
}
.summary-item hr,
.review__step hr {
border: none; //reset
border-top: 1px solid color('primary-dark');
margin-top: 0;
margin-bottom: units(0.5);
}
.review__step__title a:visited {
color: color('primary');
}
.review__step__name {
color: color('primary-dark');
font-weight: font-weight('semibold');
margin-bottom: units(0.5);
}

View file

@ -0,0 +1,30 @@
@use "uswds-core" as *;
.usa-sidenav {
.usa-sidenav__item {
span {
a.link_usa-checked {
padding: 0;
}
}
}
}
.sidenav__step--locked {
color: color('base-darker');
span {
display: flex;
align-items: flex-start;
padding: units(1);
.usa-icon {
flex-shrink: 0;
//align lock body to x-height
margin: units('2px') units(1) 0 0;
}
}
}
.stepnav {
margin-top: units(2);
}

View file

@ -0,0 +1,93 @@
@use "uswds-core" as *;
.dotgov-table--stacked {
td, th {
padding: units(1) units(2) units(2px) 0;
border: none;
}
tr:first-child th:first-child {
border-top: none;
}
tr {
border-bottom: none;
border-top: 2px solid color('base-light');
margin-top: units(2);
&:first-child {
margin-top: 0;
}
}
td[data-label]:before,
th[data-label]:before {
color: color('primary-darker');
padding-bottom: units(2px);
}
}
.dotgov-table {
width: 100%;
a {
display: flex;
align-items: flex-start;
color: color('primary');
&:visited {
color: color('primary');
}
.usa-icon {
// align icon with x height
margin-top: units(0.5);
margin-right: units(0.5);
}
}
th[data-sortable]:not([aria-sort]) .usa-table__header__button {
right: auto;
}
tbody th {
word-break: break-word;
}
@include at-media(mobile-lg) {
margin-top: units(1);
tr {
border: none;
}
td, th {
border-bottom: 1px solid color('base-light');
}
thead th {
color: color('primary-darker');
border-bottom: 2px solid color('base-light');
}
tbody tr:last-of-type {
td, th {
border-bottom: 0;
}
}
td, th,
.usa-tabel th{
padding: units(2) units(2) units(2) 0;
}
th:first-of-type {
padding-left: 0;
}
thead tr:first-child th:first-child {
border-top: none;
}
}
}

View file

@ -0,0 +1,24 @@
@use "uswds-core" as *;
// Finer grained letterspacing adjustments
$letter-space--xs: .0125em;
p,
address,
.usa-list li {
@include typeset('sans', 'sm', 5);
max-width: measure(5);
}
h1 {
@include typeset('sans', '2xl', 2);
margin: 0 0 units(2);
color: color('primary-darker');
}
h2 {
font-weight: font-weight('semibold');
line-height: line-height('heading', 3);
margin: units(4) 0 units(1);
color: color('primary-darker');
}

View file

@ -1,457 +0,0 @@
/*
* * * * * ==============================
* * * * * ==============================
* * * * * ==============================
* * * * * ==============================
========================================
========================================
========================================
----------------------------------------
USWDS THEME CUSTOM STYLES
----------------------------------------
!! Copy this file to your project's
sass root. Don't edit the version
in node_modules.
----------------------------------------
Custom project SASS goes here.
i.e.
@include u-padding-right('05');
----------------------------------------
*/
// Finer grained letterspacing adjustments
$letter-space--xs: .0125em;
@use "uswds-core" as *;
/* Styles for making visible to screen reader / AT users only. */
.sr-only {
@include sr-only;
}
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
#wrapper {
flex-grow: 1;
}
.usa-logo {
@include at-media(desktop) {
margin-top: units(2);
}
}
.usa-logo__text {
@include typeset('sans', 'xl', 2);
color: color('primary-darker');
}
.usa-nav__primary {
margin-top: units(1);
}
p,
address,
.usa-list li {
@include typeset('sans', 'sm', 5);
max-width: measure(5);
}
h1 {
@include typeset('sans', '2xl', 2);
margin: 0 0 units(2);
color: color('primary-darker');
}
h2 {
font-weight: font-weight('semibold');
line-height: line-height('heading', 3);
margin: units(4) 0 units(1);
color: color('primary-darker');
}
.register-form-step > h1 {
//align to top of sidebar on first page of the form
margin-top: units(-1);
}
//Tighter spacing when H2 is immediatly after H1
.register-form-step .usa-fieldset:first-of-type h2:first-of-type,
.register-form-step h1 + h2 {
margin-top: units(1);
}
.register-form-step h3 {
color: color('primary-dark');
letter-spacing: $letter-space--xs;
margin-top: units(3);
margin-bottom: 0;
+ p {
margin-top: units(0.5);
}
}
.register-form-step h4 {
margin-bottom: 0;
+ p {
margin-top: units(0.5);
}
}
.register-form-step a {
color: color('primary');
&:visited {
color: color('violet-70v'); //USWDS default
}
}
.register-form-step .usa-form-group:first-of-type,
.register-form-step .usa-label:first-of-type {
margin-top: units(1);
}
/* Make "placeholder" links visually obvious */
a[href$="todo"]::after {
background-color: yellow;
color: color(blue-80v);
content: " [link TBD]";
font-style: italic;
}
a.breadcrumb__back {
display:flex;
align-items: center;
margin-bottom: units(2.5);
&:visited {
color: color('primary');
}
@include at-media('tablet') {
//align to top of sidebar
margin-top: units(-0.5);
}
}
a.withdraw {
background-color: color('error');
}
a.withdraw_outline,
a.withdraw_outline:visited {
box-shadow: inset 0 0 0 2px color('error');
color: color('error');
}
a.withdraw_outline:hover,
a.withdraw_outline:focus {
box-shadow: inset 0 0 0 2px color('error-dark');
color: color('error-dark');
}
a.withdraw_outline:active {
box-shadow: inset 0 0 0 2px color('error-darker');
color: color('error-darker');
}
a.withdraw:hover,
a.withdraw:focus {
background-color: color('error-dark');
}
a.withdraw:active {
background-color: color('error-darker');
}
.usa-sidenav {
.usa-sidenav__item {
span {
a.link_usa-checked {
padding: 0;
}
}
}
}
.sidenav__step--locked {
color: color('base-darker');
span {
display: flex;
align-items: flex-start;
padding: units(1);
.usa-icon {
flex-shrink: 0;
//align lock body to x-height
margin: units('2px') units(1) 0 0;
}
}
}
.stepnav {
margin-top: units(2);
}
.ao_example p {
margin-top: units(1);
}
.domain_example {
p {
margin-bottom: 0;
}
.usa-list {
margin-top: units(0.5);
}
}
.review__step {
margin-top: units(3);
}
.summary-item hr,
.review__step hr {
border: none; //reset
border-top: 1px solid color('primary-dark');
margin-top: 0;
margin-bottom: units(0.5);
}
.review__step__title a:visited {
color: color('primary');
}
.review__step__name {
color: color('primary-dark');
font-weight: font-weight('semibold');
margin-bottom: units(0.5);
}
.usa-form .usa-button {
margin-top: units(3);
}
.usa-button--unstyled .usa-icon {
vertical-align: bottom;
}
a.usa-button--unstyled:visited {
color: color('primary');
}
.dotgov-button--green {
background-color: color('success-dark');
&:hover {
background-color: color('success-darker');
}
&:active {
background-color: color('green-80v');
}
}
/** ---- DASHBOARD ---- */
#wrapper.dashboard {
background-color: color('primary-lightest');
padding-top: units(5);
}
.section--outlined {
background-color: color('white');
border: 1px solid color('base-lighter');
border-radius: 4px;
padding: 0 units(2) units(3);
margin-top: units(3);
h2 {
color: color('primary-dark');
margin-top: units(2);
margin-bottom: units(2);
}
p {
margin-bottom: 0;
}
@include at-media(mobile-lg) {
margin-top: units(5);
h2 {
margin-bottom: 0;
}
}
}
.dotgov-table--stacked {
td, th {
padding: units(1) units(2) units(2px) 0;
border: none;
}
tr:first-child th:first-child {
border-top: none;
}
tr {
border-bottom: none;
border-top: 2px solid color('base-light');
margin-top: units(2);
&:first-child {
margin-top: 0;
}
}
td[data-label]:before,
th[data-label]:before {
color: color('primary-darker');
padding-bottom: units(2px);
}
}
.dotgov-table {
width: 100%;
a {
display: flex;
align-items: flex-start;
color: color('primary');
&:visited {
color: color('primary');
}
.usa-icon {
// align icon with x height
margin-top: units(0.5);
margin-right: units(0.5);
}
}
th[data-sortable]:not([aria-sort]) .usa-table__header__button {
right: auto;
}
tbody th {
word-break: break-word;
}
@include at-media(mobile-lg) {
margin-top: units(1);
tr {
border: none;
}
td, th {
border-bottom: 1px solid color('base-light');
}
thead th {
color: color('primary-darker');
border-bottom: 2px solid color('base-light');
}
tbody tr:last-of-type {
td, th {
border-bottom: 0;
}
}
td, th,
.usa-tabel th{
padding: units(2) units(2) units(2) 0;
}
th:first-of-type {
padding-left: 0;
}
thead tr:first-child th:first-child {
border-top: none;
}
}
}
.break-word {
word-break: break-word;
}
.dotgov-status-box {
background-color: color('primary-lightest');
border-color: color('accent-cool-lighter');
}
.dotgov-status-box--action-need {
background-color: color('warning-lighter');
border-color: color('warning');
}
#wrapper {
padding-top: units(3);
padding-bottom: units(6) * 2 ; //Workaround because USWDS units jump from 10 to 15
}
footer {
border-top: 1px solid color('primary-darker');
}
.usa-footer__secondary-section {
background-color: color('primary-lightest');
}
.usa-footer__secondary-section a {
color: color('primary');
}
.usa-identifier__logo {
height: units(7);
}
abbr[title] {
// workaround for underlining abbr element
border-bottom: none;
text-decoration: none;
}
.usa-textarea {
@include at-media('tablet') {
height: units('mobile');
}
}
// Fixes some font size disparities with the Figma
// for usa-alert alert elements
.usa-alert {
.usa-alert__heading.larger-font-sizing {
font-size: units(3);
}
}
// The icon was off center for some reason
// Fixes that issue
@media (min-width: 64em){
.usa-alert--warning{
.usa-alert__body::before {
left: 1rem !important;
}
}
}

View file

@ -8,7 +8,15 @@
/*--------------------------------------------------
--- Custom Styles ---------------------------------*/
@forward "uswds-theme-custom-styles";
@forward "base";
@forward "typography";
@forward "buttons";
@forward "forms";
@forward "fieldsets";
@forward "alerts";
@forward "tables";
@forward "sidenav";
@forward "register-form";
/*--------------------------------------------------
--- Admin ---------------------------------*/

View file

@ -81,9 +81,29 @@ urlpatterns = [
path("domain/<int:pk>", views.DomainView.as_view(), name="domain"),
path("domain/<int:pk>/users", views.DomainUsersView.as_view(), name="domain-users"),
path(
"domain/<int:pk>/nameservers",
"domain/<int:pk>/dns",
views.DomainDNSView.as_view(),
name="domain-dns",
),
path(
"domain/<int:pk>/dns/nameservers",
views.DomainNameserversView.as_view(),
name="domain-nameservers",
name="domain-dns-nameservers",
),
path(
"domain/<int:pk>/dns/dnssec",
views.DomainDNSSECView.as_view(),
name="domain-dns-dnssec",
),
path(
"domain/<int:pk>/dns/dnssec/dsdata",
views.DomainDsDataView.as_view(),
name="domain-dns-dnssec-dsdata",
),
path(
"domain/<int:pk>/dns/dnssec/keydata",
views.DomainKeyDataView.as_view(),
name="domain-dns-dnssec-keydata",
),
path(
"domain/<int:pk>/your-contact-information",

View file

@ -5,4 +5,9 @@ from .domain import (
DomainSecurityEmailForm,
DomainOrgNameAddressForm,
ContactForm,
DomainDnssecForm,
DomainDsdataFormset,
DomainDsdataForm,
DomainKeydataFormset,
DomainKeydataForm,
)

View file

@ -0,0 +1,38 @@
# common.py
#
# ALGORITHM_CHOICES are options for alg attribute in DS Data and Key Data
# reference:
# https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml
ALGORITHM_CHOICES = [
(1, "(1) ERSA/MD5 [RSAMD5]"),
(2, "(2) Diffie-Hellman [DH]"),
(3, "(3) DSA/SHA-1 [DSA]"),
(5, "(5) RSA/SHA-1 [RSASHA1]"),
(6, "(6) DSA-NSEC3-SHA1"),
(7, "(7) RSASHA1-NSEC3-SHA1"),
(8, "(8) RSA/SHA-256 [RSASHA256]"),
(10, "(10) RSA/SHA-512 [RSASHA512]"),
(12, "(12) GOST R 34.10-2001 [ECC-GOST]"),
(13, "(13) ECDSA Curve P-256 with SHA-256 [ECDSAP256SHA256]"),
(14, "(14) ECDSA Curve P-384 with SHA-384 [ECDSAP384SHA384]"),
(15, "(15) Ed25519"),
(16, "(16) Ed448"),
]
# DIGEST_TYPE_CHOICES are options for digestType attribute in DS Data
# reference: https://datatracker.ietf.org/doc/html/rfc4034#appendix-A.2
DIGEST_TYPE_CHOICES = [
(0, "(0) Reserved"),
(1, "(1) SHA-256"),
]
# PROTOCOL_CHOICES are options for protocol attribute in Key Data
# reference: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.2
PROTOCOL_CHOICES = [
(3, "(3) DNSSEC"),
]
# FLAG_CHOICES are options for flags attribute in Key Data
# reference: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.1
FLAG_CHOICES = [
(0, "(0)"),
(256, "(256) ZSK"),
(257, "(257) KSK"),
]

View file

@ -1,23 +1,27 @@
"""Forms for domain management."""
from django import forms
from django.core.validators import RegexValidator
from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator
from django.forms import formset_factory
from phonenumber_field.widgets import RegionalPhoneNumberWidget
from ..models import Contact, DomainInformation
from .common import (
ALGORITHM_CHOICES,
DIGEST_TYPE_CHOICES,
FLAG_CHOICES,
PROTOCOL_CHOICES,
)
class DomainAddUserForm(forms.Form):
"""Form for adding a user to a domain."""
email = forms.EmailField(label="Email")
class DomainNameserverForm(forms.Form):
"""Form for changing nameservers."""
server = forms.CharField(label="Name server", strip=True)
@ -31,7 +35,6 @@ NameserverFormset = formset_factory(
class ContactForm(forms.ModelForm):
"""Form for updating contacts."""
class Meta:
@ -62,14 +65,12 @@ class ContactForm(forms.ModelForm):
class DomainSecurityEmailForm(forms.Form):
"""Form for adding or editing a security email to a domain."""
security_email = forms.EmailField(label="Security email", required=False)
class DomainOrgNameAddressForm(forms.ModelForm):
"""Form for updating the organization name and mailing address."""
zipcode = forms.CharField(
@ -140,3 +141,91 @@ class DomainOrgNameAddressForm(forms.ModelForm):
self.fields[field_name].required = True
self.fields["state_territory"].widget.attrs.pop("maxlength", None)
self.fields["zipcode"].widget.attrs.pop("maxlength", None)
class DomainDnssecForm(forms.Form):
"""Form for enabling and disabling dnssec"""
class DomainDsdataForm(forms.Form):
"""Form for adding or editing DNSSEC DS Data to a domain."""
key_tag = forms.IntegerField(
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"),
],
error_messages={"required": ("Key tag is required.")},
)
algorithm = forms.TypedChoiceField(
required=True,
label="Algorithm",
coerce=int, # need to coerce into int so dsData objects can be compared
choices=[(None, "--Select--")] + ALGORITHM_CHOICES, # type: ignore
error_messages={"required": ("Algorithm is required.")},
)
digest_type = forms.TypedChoiceField(
required=True,
label="Digest type",
coerce=int, # need to coerce into int so dsData objects can be compared
choices=[(None, "--Select--")] + DIGEST_TYPE_CHOICES, # type: ignore
error_messages={"required": ("Digest Type is required.")},
)
digest = forms.CharField(
required=True,
label="Digest",
error_messages={"required": ("Digest is required.")},
)
DomainDsdataFormset = formset_factory(
DomainDsdataForm,
extra=0,
can_delete=True,
)
class DomainKeydataForm(forms.Form):
"""Form for adding or editing DNSSEC Key Data to a domain."""
flag = forms.TypedChoiceField(
required=True,
label="Flag",
coerce=int,
choices=FLAG_CHOICES,
error_messages={"required": ("Flag is required.")},
)
protocol = forms.TypedChoiceField(
required=True,
label="Protocol",
coerce=int,
choices=PROTOCOL_CHOICES,
error_messages={"required": ("Protocol is required.")},
)
algorithm = forms.TypedChoiceField(
required=True,
label="Algorithm",
coerce=int,
choices=[(None, "--Select--")] + ALGORITHM_CHOICES, # type: ignore
error_messages={"required": ("Algorithm is required.")},
)
pub_key = forms.CharField(
required=True,
label="Pub key",
error_messages={"required": ("Pub key is required.")},
)
DomainKeydataFormset = formset_factory(
DomainKeydataForm,
extra=0,
can_delete=True,
)

View file

@ -4,6 +4,7 @@ import ipaddress
import re
from datetime import date
from string import digits
from typing import Optional
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
from django.db import models
@ -456,24 +457,140 @@ class Domain(TimeStampedModel, DomainHelper):
return [deleteObj], len(deleteStrList)
@Cache
def dnssecdata(self) -> extensions.DNSSECExtension:
def dnssecdata(self) -> Optional[extensions.DNSSECExtension]:
"""
Get a complete list of dnssecdata extensions for this domain.
dnssecdata are provided as a list of DNSSECExtension objects.
A DNSSECExtension object includes:
maxSigLife: Optional[int]
dsData: Optional[Sequence[DSData]]
keyData: Optional[Sequence[DNSSECKeyData]]
"""
try:
return self._get_property("dnssecdata")
except Exception as err:
# Don't throw error as this is normal for a new domain
logger.info("Domain does not have dnssec data defined %s" % err)
return None
def getDnssecdataChanges(
self, _dnssecdata: Optional[extensions.DNSSECExtension]
) -> tuple[dict, dict]:
"""
calls self.dnssecdata, it should pull from cache but may result
in an epp call
returns tuple of 2 values as follows:
addExtension: dict
remExtension: dict
addExtension includes all dsData or keyData to be added
remExtension includes all dsData or keyData to be removed
method operates on dsData OR keyData, never a mix of the two;
operates based on which is present in _dnssecdata;
if neither is present, addExtension will be empty dict, and
remExtension will be all existing dnssecdata to be deleted
"""
oldDnssecdata = self.dnssecdata
addDnssecdata: dict = {}
remDnssecdata: dict = {}
if _dnssecdata and _dnssecdata.dsData is not None:
# initialize addDnssecdata and remDnssecdata for dsData
addDnssecdata["dsData"] = _dnssecdata.dsData
if oldDnssecdata and len(oldDnssecdata.dsData) > 0:
# if existing dsData not in new dsData, mark for removal
dsDataForRemoval = [
dsData
for dsData in oldDnssecdata.dsData
if dsData not in _dnssecdata.dsData
]
if len(dsDataForRemoval) > 0:
remDnssecdata["dsData"] = dsDataForRemoval
# if new dsData not in existing dsData, mark for add
dsDataForAdd = [
dsData
for dsData in _dnssecdata.dsData
if dsData not in oldDnssecdata.dsData
]
if len(dsDataForAdd) > 0:
addDnssecdata["dsData"] = dsDataForAdd
else:
addDnssecdata["dsData"] = None
elif _dnssecdata and _dnssecdata.keyData is not None:
# initialize addDnssecdata and remDnssecdata for keyData
addDnssecdata["keyData"] = _dnssecdata.keyData
if oldDnssecdata and len(oldDnssecdata.keyData) > 0:
# if existing keyData not in new keyData, mark for removal
keyDataForRemoval = [
keyData
for keyData in oldDnssecdata.keyData
if keyData not in _dnssecdata.keyData
]
if len(keyDataForRemoval) > 0:
remDnssecdata["keyData"] = keyDataForRemoval
# if new keyData not in existing keyData, mark for add
keyDataForAdd = [
keyData
for keyData in _dnssecdata.keyData
if keyData not in oldDnssecdata.keyData
]
if len(keyDataForAdd) > 0:
addDnssecdata["keyData"] = keyDataForAdd
else:
# there are no new dsData or keyData, remove all
remDnssecdata["dsData"] = getattr(oldDnssecdata, "dsData", None)
remDnssecdata["keyData"] = getattr(oldDnssecdata, "keyData", None)
return addDnssecdata, remDnssecdata
@dnssecdata.setter # type: ignore
def dnssecdata(self, _dnssecdata: extensions.DNSSECExtension):
updateParams = {
"maxSigLife": _dnssecdata.get("maxSigLife", None),
"dsData": _dnssecdata.get("dsData", None),
"keyData": _dnssecdata.get("keyData", None),
"remAllDsKeyData": True,
def dnssecdata(self, _dnssecdata: Optional[extensions.DNSSECExtension]):
_addDnssecdata, _remDnssecdata = self.getDnssecdataChanges(_dnssecdata)
addParams = {
"maxSigLife": _addDnssecdata.get("maxSigLife", None),
"dsData": _addDnssecdata.get("dsData", None),
"keyData": _addDnssecdata.get("keyData", None),
}
request = commands.UpdateDomain(name=self.name)
extension = commands.UpdateDomainDNSSECExtension(**updateParams)
request.add_extension(extension)
remParams = {
"maxSigLife": _remDnssecdata.get("maxSigLife", None),
"remDsData": _remDnssecdata.get("dsData", None),
"remKeyData": _remDnssecdata.get("keyData", None),
}
addRequest = commands.UpdateDomain(name=self.name)
addExtension = commands.UpdateDomainDNSSECExtension(**addParams)
addRequest.add_extension(addExtension)
remRequest = commands.UpdateDomain(name=self.name)
remExtension = commands.UpdateDomainDNSSECExtension(**remParams)
remRequest.add_extension(remExtension)
try:
registry.send(request, cleaned=True)
if (
"dsData" in _addDnssecdata
and _addDnssecdata["dsData"] is not None
or "keyData" in _addDnssecdata
and _addDnssecdata["keyData"] is not None
):
registry.send(addRequest, cleaned=True)
if (
"dsData" in _remDnssecdata
and _remDnssecdata["dsData"] is not None
or "keyData" in _remDnssecdata
and _remDnssecdata["keyData"] is not None
):
registry.send(remRequest, cleaned=True)
except RegistryError as e:
logger.error("Error adding DNSSEC, code was %s error was %s" % (e.code, e))
logger.error(
"Error updating DNSSEC, code was %s error was %s" % (e.code, e)
)
raise e
@nameservers.setter # type: ignore

View file

@ -27,7 +27,7 @@
</div>
<br>
{% url 'domain-nameservers' pk=domain.id as url %}
{% url 'domain-dns-nameservers' pk=domain.id as url %}
{% if domain.nameservers|length > 0 %}
{% include "includes/summary_item.html" with title='DNS name servers' value=domain.nameservers list='true' edit_link=url %}
{% else %}

View file

@ -0,0 +1,20 @@
{% extends "domain_base.html" %}
{% load static field_helpers url_helpers %}
{% block title %}DNS | {{ domain.name }} | {% endblock %}
{% block domain_content %}
<h1>DNS</h1>
<p>The Domain Name System (DNS) is the internet service that translates your domain name into an IP address. Before your .gov domain can be used, you'll need to connect it to your DNS hosting service and provide us with your name server information.</p>
<p>You can enter your name servers, as well as other DNS-related information, in the following sections:</p>
{% url 'domain-dns-nameservers' pk=domain.id as url %}
<p><a href="{{ url }}">DNS name servers</a></p>
{% url 'domain-dnssec' pk=domain.id as url %}
<p><a href="{{ url }}">DNSSEC</a></p>
{% endblock %} {# domain_content #}

View file

@ -0,0 +1,68 @@
{% extends "domain_base.html" %}
{% load static field_helpers url_helpers %}
{% block title %}DNSSEC | {{ domain.name }} | {% endblock %}
{% block domain_content %}
<h1>DNSSEC</h1>
<p>DNSSEC, or DNS Security Extensions, is additional security layer to protect your domain. Enabling DNSSEC ensures that when someone visits your domain, they can be certain that it's connecting to the correct server, preventing potential hijacking or tampering with your domain's records.</p>
<form class="usa-form usa-form--text-width" method="post">
{% csrf_token %}
{% if has_dnssec_records %}
<div class="usa-alert usa-alert--info usa-alert--slim">
<div class="usa-alert__body">
In order to fully disable DNSSEC on your domain, you will need to work with your DNS provider to remove your DNSSEC-related records from your zone.
</div>
</div>
<a
href="#toggle-dnssec-alert"
class="usa-button"
aria-controls="toggle-dnssec-alert"
data-open-modal
>Disable DNSSEC</a
>
{% elif dnssec_enabled %}
<div id="add-records">
<h2> Add DS Records </h2>
<p>In order to enable DNSSEC and add Delegation Signer (DS) records, you must first configure it with your DNS hosting service. Your configuration will determine whether you need to add DS Data or Key Data. Contact your DNS hosting provider if you are unsure which record type to add.</p>
<p>
<a href="{% url 'domain-dns-dnssec-dsdata' pk=domain.id %}" class="usa-button usa-button--outline">Add DS Data</a>
<a href="{% url 'domain-dns-dnssec-keydata' pk=domain.id %}" class="usa-button usa-button--outline">Add Key Data</a>
<button
type="submit"
class="usa-button usa-button--unstyled usa-button--cancel"
name="cancel_dnssec"
id="cancel_dnssec"
>Cancel</button>
</p>
</div>
{% else %}
<div id="enable-dnssec">
<div class="usa-alert usa-alert--info usa-alert--slim">
<div class="usa-alert__body">
It is strongly recommended that you only enable DNSSEC if you know how to set it up properly at your hosting service. If you make a mistake, it could cause your domain name to stop working.
</div>
</div>
<button
type="submit"
class="usa-button"
name="enable_dnssec"
id="enable_dnssec"
>Enable DNSSEC</button>
</div>
{% endif %}
</form>
<div
class="usa-modal"
id="toggle-dnssec-alert"
aria-labelledby="Are you sure you want to continue?"
aria-describedby="Your DNSSEC records will be deleted from the registry."
>
{% include 'includes/modal.html' with modal_heading="Are you sure you want to continue?" modal_description="Your DNSSEC records will be deleted from the registry." modal_button=modal_button|safe %}
</div>
{% endblock %} {# domain_content #}

View file

@ -0,0 +1,123 @@
{% extends "domain_base.html" %}
{% load static field_helpers url_helpers %}
{% block title %}DS Data | {{ domain.name }} | {% endblock %}
{% block domain_content %}
{% for form in formset %}
{% include "includes/form_errors.html" with form=form %}
{% endfor %}
{% if domain.dnssecdata is None and not dnssec_ds_confirmed %}
<div class="usa-alert usa-alert--info usa-alert--slim margin-bottom-3">
<div class="usa-alert__body">
You have no DS Data added. Enable DNSSEC by adding DS Data or return to the DNSSEC page and click 'enable.'
</div>
</div>
{% endif %}
<h1>DS Data</h1>
{% if domain.dnssecdata is not None and domain.dnssecdata.keyData is not None %}
<div class="usa-alert usa-alert--warning usa-alert--slim margin-bottom-3">
<div class="usa-alert__body">
<h4 class="usa-alert__heading">Warning, you cannot add DS Data</h4>
<p class="usa-alert__text">
You cannot add DS Data because you have already added Key Data. Delete your Key Data records in order to add DS Data.
</p>
</div>
</div>
{% elif not dnssec_ds_confirmed %}
<p>In order to enable DNSSEC, you must first configure it with your DNS hosting service.</p>
<p>Enter the values given by your DNS provider for DS Data.</p>
<p>Required fields are marked with an asterisk (<abbr
title="required"
class="usa-hint usa-hint--required"
>*</abbr>).</p>
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
{% csrf_token %}
<button type="submit" class="usa-button usa-button--unstyled display-block" name="confirm-ds">
<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>
</svg><span class="margin-left-05">Add new record</span>
</button>
</form>
{% else %}
<p>Enter the values given by your DNS provider for DS Data.</p>
{% include "includes/required_fields.html" %}
<form class="usa-form usa-form--extra-large" method="post" novalidate id="form-container">
{% csrf_token %}
{{ formset.management_form }}
{% for form in formset %}
<fieldset class="ds-record">
<legend class="sr-only">DS Data record {{forloop.counter}}</legend>
<h2 class="margin-top-0">DS Data record {{forloop.counter}}</h2>
<div class="grid-row grid-gap-2 flex-end">
<div class="tablet:grid-col-4">
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.key_tag %}
{% endwith %}
</div>
<div class="tablet:grid-col-4">
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.algorithm %}
{% endwith %}
</div>
<div class="tablet:grid-col-4">
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.digest_type %}
{% endwith %}
</div>
</div>
<div class="grid-row">
<div class="grid-col">
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.digest %}
{% endwith %}
</div>
</div>
<div class="grid-row margin-top-1">
<div class="grid-col">
<button type="button" class="usa-button usa-button--unstyled display-block float-right-tablet delete-record">
<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>
</div>
</div>
</fieldset>
{% endfor %}
<button type="button" class="usa-button usa-button--unstyled display-block margin-bottom-2" id="add-ds-form">
<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>
</svg><span class="margin-left-05">Add new record</span>
</button>
<button
type="submit"
class="usa-button"
>Save
</button>
</form>
<form aria-label="form to undo changes to the DS records">
<button
type="submit"
class="usa-button usa-button--outline btn-cancel"
name="btn-cancel-click"
aria-label="Reset the data in the DS records to the registry state (undo changes)"
>Cancel
</button>
</form>
{% endif %}
{% endblock %} {# domain_content #}

View file

@ -0,0 +1,110 @@
{% extends "domain_base.html" %}
{% load static field_helpers url_helpers %}
{% block title %}Key Data | {{ domain.name }} | {% endblock %}
{% block domain_content %}
{% for form in formset %}
{% include "includes/form_errors.html" with form=form %}
{% endfor %}
<h1>Key Data</h1>
{% if domain.dnssecdata is not None and domain.dnssecdata.dsData is not None %}
<div class="usa-alert usa-alert--warning usa-alert--slim margin-bottom-3">
<div class="usa-alert__body">
<h4 class="usa-alert__heading">Warning, you cannot add Key Data</h4>
<p class="usa-alert__text">
You cannot add Key Data because you have already added DS Data. Delete your DS Data records in order to add Key Data.
</p>
</div>
</div>
{% elif not dnssec_key_confirmed %}
<p>In order to enable DNSSEC and add DS records, you must first configure it with your DNS hosting service. Your configuration will determine whether you need to add DS Data or Key Data. Contact your DNS hosting provider if you are unsure which record type to add.</p>
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
{% csrf_token %}
<button
type="submit"
class="usa-button"
name="confirm-key"
>Add DS Key record</button>
</form>
{% else %}
<p>Enter the values given by your DNS provider for DS Key Data.</p>
{% include "includes/required_fields.html" %}
<form class="usa-form usa-form--extra-large" method="post" novalidate id="form-container">
{% csrf_token %}
{{ formset.management_form }}
{% for form in formset %}
<fieldset class="ds-record">
<legend class="sr-only">DS Data record {{forloop.counter}}</legend>
<h2 class="margin-top-0">DS Data record {{forloop.counter}}</h2>
<div class="grid-row grid-gap-2 flex-end">
<div class="tablet:grid-col-4">
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.flag %}
{% endwith %}
</div>
<div class="tablet:grid-col-4">
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.protocol %}
{% endwith %}
</div>
<div class="tablet:grid-col-4">
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.algorithm %}
{% endwith %}
</div>
</div>
<div class="grid-row">
<div class="grid-col">
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
{% input_with_errors form.pub_key %}
{% endwith %}
</div>
</div>
<div class="grid-row margin-top-2">
<div class="grid-col">
<button type="button" class="usa-button usa-button--unstyled display-block float-right-tablet delete-record">
<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>
</div>
</div>
</fieldset>
{% endfor %}
<button type="button" class="usa-button usa-button--unstyled display-block margin-bottom-2" id="add-ds-form">
<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>
</svg><span class="margin-left-05">Add new record</span>
</button>
<button
type="submit"
class="usa-button"
>Save
</button>
</form>
<form aria-label="form to undo changes to the DS records">
<button
type="submit"
class="usa-button usa-button--outline btn-cancel"
name="btn-cancel-click"
aria-label="Reset the data in the DS records to the registry state (undo changes)"
>Cancel
</button>
</form>
{% endif %}
{% endblock %} {# domain_content #}

View file

@ -34,7 +34,7 @@
</div>
{% endfor %}
<button type="button" class="usa-button usa-button--unstyled display-block" id="add-form">
<button type="button" class="usa-button usa-button--unstyled display-block" id="add-nameserver-form">
<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>
</svg><span class="margin-left-05">Add another name server</span>

View file

@ -13,14 +13,52 @@
</li>
<li class="usa-sidenav__item">
{% url 'domain-nameservers' pk=domain.id as url %}
{% url 'domain-dns' pk=domain.id as url %}
<a href="{{ url }}" {% if request.path|startswith:url %}class="usa-current"{% endif %}>
DNS
</a>
<ul class="usa-sidenav__sublist">
<li class="usa-sidenav__item">
{% url 'domain-dns-nameservers' pk=domain.id as url %}
<a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %}
>
DNS name servers
Name servers
</a>
</li>
<li class="usa-sidenav__item">
{% url 'domain-dns-dnssec' pk=domain.id as url %}
<a href="{{ url }}"
{% if request.path|startswith:url %}class="usa-current"{% endif %}
>
DNSSEC
</a>
{% if domain.dnssecdata is not None or request.path|startswith:url and request.path|endswith:'data' %}
<ul class="usa-sidenav__sublist">
<li class="usa-sidenav__item">
{% url 'domain-dns-dnssec-dsdata' pk=domain.id as url %}
<a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %}
>
DS Data
</a>
</li>
<li class="usa-sidenav__item">
{% url 'domain-dns-dnssec-keydata' pk=domain.id as url %}
<a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %}
>
DS Key Data
</a>
</li>
</ul>
{% endif %}
</li>
</ul>
</li>
<li class="usa-sidenav__item">
{% url 'domain-org-name-address' pk=domain.id as url %}
<a href="{{ url }}"

View file

@ -0,0 +1,42 @@
<div class="usa-modal__content">
<div class="usa-modal__main">
<h2 class="usa-modal__heading" id="modal-1-heading">
{{ modal_heading }}
</h2>
<div class="usa-prose">
<p id="modal-1-description">
{{ modal_description }}
</p>
</div>
<div class="usa-modal__footer">
<ul class="usa-button-group">
<li class="usa-button-group__item">
<form method="post">
{% csrf_token %}
{{ modal_button }}
</form>
</li>
<li class="usa-button-group__item">
<button
type="button"
class="usa-button usa-button--unstyled padding-105 text-center"
data-close-modal
>
Cancel
</button>
</li>
</ul>
</div>
</div>
<button
type="button"
class="usa-button usa-modal__close"
aria-label="Close this window"
data-close-modal
>
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
<use xlink:href="/assets/img/sprite.svg#close"></use>
</svg>
</button>
</div>

View file

@ -149,7 +149,7 @@ def input_with_errors(context, field=None): # noqa: C901
# see Widget.get_context() on
# https://docs.djangoproject.com/en/4.1/ref/forms/widgets
widget = field.field.widget.get_context(
field.html_name, field.value() or field.initial, field.build_widget_attrs(attrs)
field.html_name, field.value(), field.build_widget_attrs(attrs)
) # -> {"widget": {"name": ...}}
context["widget"] = widget["widget"]

View file

@ -19,6 +19,13 @@ def startswith(text, starts):
return False
@register.filter("endswith")
def endswith(text, ends):
if isinstance(text, str):
return text.endswith(ends)
return False
@register.simple_tag
def public_site_url(url_path):
"""Make a full URL for this path at our public site.

View file

@ -27,6 +27,7 @@ from registrar.models import (
from epplibwrapper import (
commands,
common,
extensions,
info,
RegistryError,
ErrorCode,
@ -721,6 +722,45 @@ class MockEppLib(TestCase):
mockDataHostChange = fakedEppObject(
"lastPw", cr_date=datetime.datetime(2023, 8, 25, 19, 45, 35)
)
addDsData1 = {
"keyTag": 1234,
"alg": 3,
"digestType": 1,
"digest": "ec0bdd990b39feead889f0ba613db4adec0bdd99",
}
addDsData2 = {
"keyTag": 2345,
"alg": 3,
"digestType": 1,
"digest": "ec0bdd990b39feead889f0ba613db4adecb4adec",
}
keyDataDict = {
"flags": 257,
"protocol": 3,
"alg": 1,
"pubKey": "AQPJ////4Q==",
}
dnssecExtensionWithDsData = extensions.DNSSECExtension(
**{
"dsData": [
common.DSData(**addDsData1) # type: ignore
], # type: ignore
}
)
dnssecExtensionWithMultDsData = extensions.DNSSECExtension(
**{
"dsData": [
common.DSData(**addDsData1), # type: ignore
common.DSData(**addDsData2), # type: ignore
], # type: ignore
}
)
dnssecExtensionWithKeyData = extensions.DNSSECExtension(
**{
"keyData": [common.DNSSECKeyData(**keyDataDict)], # type: ignore
}
)
dnssecExtensionRemovingDsData = extensions.DNSSECExtension()
infoDomainHasIP = fakedEppObject(
"nameserverwithip.gov",
@ -744,50 +784,17 @@ class MockEppLib(TestCase):
],
)
def _getattrInfoDomain(self, _request):
if getattr(_request, "name", None) == "security.gov":
return MagicMock(res_data=[self.infoDomainNoContact])
elif getattr(_request, "name", None) == "my-nameserver.gov":
if self.mockedSendFunction.call_count == 5:
return MagicMock(res_data=[self.infoDomainTwoHosts])
else:
return MagicMock(res_data=[self.infoDomainNoHost])
elif getattr(_request, "name", None) == "nameserverwithip.gov":
return MagicMock(res_data=[self.infoDomainHasIP])
elif getattr(_request, "name", None) == "namerserversubdomain.gov":
return MagicMock(res_data=[self.infoDomainCheckHostIPCombo])
elif getattr(_request, "name", None) == "freeman.gov":
return MagicMock(res_data=[self.InfoDomainWithContacts])
elif getattr(_request, "name", None) == "threenameserversDomain.gov":
return MagicMock(res_data=[self.infoDomainThreeHosts])
return MagicMock(res_data=[self.mockDataInfoDomain])
def mockSend(self, _request, cleaned):
"""Mocks the registry.send function used inside of domain.py
registry is imported from epplibwrapper
returns objects that simulate what would be in a epp response
but only relevant pieces for tests"""
if isinstance(_request, commands.InfoDomain):
return self._getattrInfoDomain(_request)
return self.mockInfoDomainCommands(_request, cleaned)
elif isinstance(_request, commands.InfoContact):
mocked_result: info.InfoContactResultData
# For testing contact types
match getattr(_request, "id", None):
case "securityContact":
mocked_result = self.mockSecurityContact
case "technicalContact":
mocked_result = self.mockTechnicalContact
case "adminContact":
mocked_result = self.mockAdministrativeContact
case "regContact":
mocked_result = self.mockRegistrantContact
case _:
# Default contact return
mocked_result = self.mockDataInfoContact
return MagicMock(res_data=[mocked_result])
return self.mockInfoContactCommands(_request, cleaned)
elif isinstance(_request, commands.UpdateDomain):
return self.mockUpdateDomainCommands(_request, cleaned)
elif (
isinstance(_request, commands.CreateContact)
and getattr(_request, "id", None) == "fail"
@ -820,11 +827,6 @@ class MockEppLib(TestCase):
res_data=[self.mockDataHostChange],
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
)
elif isinstance(_request, commands.UpdateDomain):
return MagicMock(
res_data=[self.mockDataHostChange],
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
)
elif isinstance(_request, commands.DeleteHost):
return MagicMock(
res_data=[self.mockDataHostChange],
@ -840,9 +842,77 @@ class MockEppLib(TestCase):
raise RegistryError(
code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION
)
return MagicMock(res_data=[self.mockDataInfoHosts])
def mockUpdateDomainCommands(self, _request, cleaned):
if getattr(_request, "name", None) == "dnssec-invalid.gov":
raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR)
else:
return MagicMock(
res_data=[self.mockDataHostChange],
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
)
def mockInfoDomainCommands(self, _request, cleaned):
request_name = getattr(_request, "name", None)
# Define a dictionary to map request names to data and extension values
request_mappings = {
"security.gov": (self.infoDomainNoContact, None),
"dnssec-dsdata.gov": (
self.mockDataInfoDomain,
self.dnssecExtensionWithDsData,
),
"dnssec-multdsdata.gov": (
self.mockDataInfoDomain,
self.dnssecExtensionWithMultDsData,
),
"dnssec-keydata.gov": (
self.mockDataInfoDomain,
self.dnssecExtensionWithKeyData,
),
"dnssec-none.gov": (self.mockDataInfoDomain, None),
"my-nameserver.gov": (
self.infoDomainTwoHosts
if self.mockedSendFunction.call_count == 5
else self.infoDomainNoHost,
None,
),
"nameserverwithip.gov": (self.infoDomainHasIP, None),
"namerserversubdomain.gov": (self.infoDomainCheckHostIPCombo, None),
"freeman.gov": (self.InfoDomainWithContacts, None),
"threenameserversDomain.gov": (self.infoDomainThreeHosts, None),
}
# Retrieve the corresponding values from the dictionary
res_data, extensions = request_mappings.get(
request_name, (self.mockDataInfoDomain, None)
)
return MagicMock(
res_data=[res_data],
extensions=[extensions] if extensions is not None else [],
)
def mockInfoContactCommands(self, _request, cleaned):
mocked_result: info.InfoContactResultData
# For testing contact types
match getattr(_request, "id", None):
case "securityContact":
mocked_result = self.mockSecurityContact
case "technicalContact":
mocked_result = self.mockTechnicalContact
case "adminContact":
mocked_result = self.mockAdministrativeContact
case "regContact":
mocked_result = self.mockRegistrantContact
case _:
# Default contact return
mocked_result = self.mockDataInfoContact
return MagicMock(res_data=[mocked_result])
def setUp(self):
"""mock epp send function as this will fail locally"""
self.mockSendPatch = patch("registrar.models.domain.registry.send")

View file

@ -3,7 +3,6 @@ Feature being tested: Registry Integration
This file tests the various ways in which the registrar interacts with the registry.
"""
from typing import Mapping, Any
from django.test import TestCase
from django.db.utils import IntegrityError
from unittest.mock import MagicMock, patch, call
@ -1442,14 +1441,26 @@ class TestRegistrantDNSSEC(MockEppLib):
"""Rule: Registrants may modify their secure DNS data"""
# helper function to create UpdateDomainDNSSECExtention object for verification
def createUpdateExtension(self, dnssecdata: extensions.DNSSECExtension):
def createUpdateExtension(
self, dnssecdata: extensions.DNSSECExtension, remove=False
):
if not remove:
return commands.UpdateDomainDNSSECExtension(
maxSigLife=dnssecdata.maxSigLife,
dsData=dnssecdata.dsData,
keyData=dnssecdata.keyData,
remDsData=None,
remKeyData=None,
remAllDsKeyData=True,
remAllDsKeyData=False,
)
else:
return commands.UpdateDomainDNSSECExtension(
maxSigLife=dnssecdata.maxSigLife,
dsData=None,
keyData=None,
remDsData=dnssecdata.dsData,
remKeyData=dnssecdata.keyData,
remAllDsKeyData=False,
)
def setUp(self):
@ -1461,37 +1472,6 @@ class TestRegistrantDNSSEC(MockEppLib):
super().setUp()
# for the tests, need a domain in the unknown state
self.domain, _ = Domain.objects.get_or_create(name="fake.gov")
self.addDsData1 = {
"keyTag": 1234,
"alg": 3,
"digestType": 1,
"digest": "ec0bdd990b39feead889f0ba613db4adec0bdd99",
}
self.addDsData2 = {
"keyTag": 2345,
"alg": 3,
"digestType": 1,
"digest": "ec0bdd990b39feead889f0ba613db4adecb4adec",
}
self.keyDataDict = {
"flags": 257,
"protocol": 3,
"alg": 1,
"pubKey": "AQPJ////4Q==",
}
self.dnssecExtensionWithDsData: Mapping[str, Any] = {
"dsData": [common.DSData(**self.addDsData1)]
}
self.dnssecExtensionWithMultDsData: Mapping[str, Any] = {
"dsData": [
common.DSData(**self.addDsData1),
common.DSData(**self.addDsData2),
],
}
self.dnssecExtensionWithKeyData: Mapping[str, Any] = {
"maxSigLife": 3215,
"keyData": [common.DNSSECKeyData(**self.keyDataDict)],
}
def tearDown(self):
Domain.objects.all().delete()
@ -1499,51 +1479,62 @@ class TestRegistrantDNSSEC(MockEppLib):
def test_user_adds_dnssec_data(self):
"""
Scenario: Registrant adds DNSSEC data.
Scenario: Registrant adds DNSSEC ds data.
Verify that both the setter and getter are functioning properly
This test verifies:
1 - setter calls UpdateDomain command
2 - setter adds the UpdateDNSSECExtension extension to the command
3 - setter causes the getter to call info domain on next get from cache
4 - getter properly parses dnssecdata from InfoDomain response and sets to cache
1 - setter initially calls InfoDomain command
2 - setter then calls UpdateDomain command
3 - setter adds the UpdateDNSSECExtension extension to the command
4 - setter causes the getter to call info domain on next get from cache
5 - getter properly parses dnssecdata from InfoDomain response and sets to cache
"""
# make sure to stop any other patcher so there are no conflicts
self.mockSendPatch.stop()
# need to use a separate patcher and side_effect for this test, as
# response from InfoDomain must be different for different iterations
# of the same command
def side_effect(_request, cleaned):
if isinstance(_request, commands.InfoDomain):
if mocked_send.call_count == 1:
return MagicMock(res_data=[self.mockDataInfoDomain])
else:
return MagicMock(
res_data=[self.mockDataInfoDomain],
extensions=[
extensions.DNSSECExtension(**self.dnssecExtensionWithDsData)
],
extensions=[self.dnssecExtensionWithDsData],
)
else:
return MagicMock(res_data=[self.mockDataInfoHosts])
patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start()
mocked_send.side_effect = side_effect
self.domain.dnssecdata = self.dnssecExtensionWithDsData
domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov")
domain.dnssecdata = self.dnssecExtensionWithDsData
# get the DNS SEC extension added to the UpdateDomain command and
# verify that it is properly sent
# args[0] is the _request sent to registry
args, _ = mocked_send.call_args
# assert that the extension matches
# assert that the extension on the update matches
self.assertEquals(
args[0].extensions[0],
self.createUpdateExtension(
extensions.DNSSECExtension(**self.dnssecExtensionWithDsData)
),
self.createUpdateExtension(self.dnssecExtensionWithDsData),
)
# test that the dnssecdata getter is functioning properly
dnssecdata_get = self.domain.dnssecdata
dnssecdata_get = domain.dnssecdata
mocked_send.assert_has_calls(
[
call(
commands.InfoDomain(
name="dnssec-dsdata.gov",
),
cleaned=True,
),
call(
commands.UpdateDomain(
name="fake.gov",
name="dnssec-dsdata.gov",
nsset=None,
keyset=None,
registrant=None,
@ -1553,16 +1544,14 @@ class TestRegistrantDNSSEC(MockEppLib):
),
call(
commands.InfoDomain(
name="fake.gov",
name="dnssec-dsdata.gov",
),
cleaned=True,
),
]
)
self.assertEquals(
dnssecdata_get.dsData, self.dnssecExtensionWithDsData["dsData"]
)
self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData)
patcher.stop()
@ -1575,48 +1564,52 @@ class TestRegistrantDNSSEC(MockEppLib):
# registry normally sends in this case
This test verifies:
1 - UpdateDomain command called twice
2 - setter causes the getter to call info domain on next get from cache
3 - getter properly parses dnssecdata from InfoDomain response and sets to cache
1 - InfoDomain command is called first
2 - UpdateDomain command called on the initial setter
3 - setter causes the getter to call info domain on next get from cache
4 - UpdateDomain command is not called on second setter (no change)
5 - getter properly parses dnssecdata from InfoDomain response and sets to cache
"""
# make sure to stop any other patcher so there are no conflicts
self.mockSendPatch.stop()
# need to use a separate patcher and side_effect for this test, as
# response from InfoDomain must be different for different iterations
# of the same command
def side_effect(_request, cleaned):
if isinstance(_request, commands.InfoDomain):
if mocked_send.call_count == 1:
return MagicMock(res_data=[self.mockDataInfoDomain])
else:
return MagicMock(
res_data=[self.mockDataInfoDomain],
extensions=[
extensions.DNSSECExtension(**self.dnssecExtensionWithDsData)
],
extensions=[self.dnssecExtensionWithDsData],
)
else:
return MagicMock(res_data=[self.mockDataInfoHosts])
patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start()
mocked_send.side_effect = side_effect
domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov")
# set the dnssecdata once
self.domain.dnssecdata = self.dnssecExtensionWithDsData
domain.dnssecdata = self.dnssecExtensionWithDsData
# set the dnssecdata again
self.domain.dnssecdata = self.dnssecExtensionWithDsData
domain.dnssecdata = self.dnssecExtensionWithDsData
# test that the dnssecdata getter is functioning properly
dnssecdata_get = self.domain.dnssecdata
dnssecdata_get = domain.dnssecdata
mocked_send.assert_has_calls(
[
call(
commands.UpdateDomain(
name="fake.gov",
nsset=None,
keyset=None,
registrant=None,
auth_info=None,
commands.InfoDomain(
name="dnssec-dsdata.gov",
),
cleaned=True,
),
call(
commands.UpdateDomain(
name="fake.gov",
name="dnssec-dsdata.gov",
nsset=None,
keyset=None,
registrant=None,
@ -1626,16 +1619,20 @@ class TestRegistrantDNSSEC(MockEppLib):
),
call(
commands.InfoDomain(
name="fake.gov",
name="dnssec-dsdata.gov",
),
cleaned=True,
),
call(
commands.InfoDomain(
name="dnssec-dsdata.gov",
),
cleaned=True,
),
]
)
self.assertEquals(
dnssecdata_get.dsData, self.dnssecExtensionWithDsData["dsData"]
)
self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData)
patcher.stop()
@ -1652,22 +1649,28 @@ class TestRegistrantDNSSEC(MockEppLib):
"""
# make sure to stop any other patcher so there are no conflicts
self.mockSendPatch.stop()
# need to use a separate patcher and side_effect for this test, as
# response from InfoDomain must be different for different iterations
# of the same command
def side_effect(_request, cleaned):
if isinstance(_request, commands.InfoDomain):
if mocked_send.call_count == 1:
return MagicMock(res_data=[self.mockDataInfoDomain])
else:
return MagicMock(
res_data=[self.mockDataInfoDomain],
extensions=[
extensions.DNSSECExtension(**self.dnssecExtensionWithMultDsData)
],
extensions=[self.dnssecExtensionWithMultDsData],
)
else:
return MagicMock(res_data=[self.mockDataInfoHosts])
patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start()
mocked_send.side_effect = side_effect
self.domain.dnssecdata = self.dnssecExtensionWithMultDsData
domain, _ = Domain.objects.get_or_create(name="dnssec-multdsdata.gov")
domain.dnssecdata = self.dnssecExtensionWithMultDsData
# get the DNS SEC extension added to the UpdateDomain command
# and verify that it is properly sent
# args[0] is the _request sent to registry
@ -1675,17 +1678,15 @@ class TestRegistrantDNSSEC(MockEppLib):
# assert that the extension matches
self.assertEquals(
args[0].extensions[0],
self.createUpdateExtension(
extensions.DNSSECExtension(**self.dnssecExtensionWithMultDsData)
),
self.createUpdateExtension(self.dnssecExtensionWithMultDsData),
)
# test that the dnssecdata getter is functioning properly
dnssecdata_get = self.domain.dnssecdata
dnssecdata_get = domain.dnssecdata
mocked_send.assert_has_calls(
[
call(
commands.UpdateDomain(
name="fake.gov",
name="dnssec-multdsdata.gov",
nsset=None,
keyset=None,
registrant=None,
@ -1695,7 +1696,7 @@ class TestRegistrantDNSSEC(MockEppLib):
),
call(
commands.InfoDomain(
name="fake.gov",
name="dnssec-multdsdata.gov",
),
cleaned=True,
),
@ -1703,14 +1704,103 @@ class TestRegistrantDNSSEC(MockEppLib):
)
self.assertEquals(
dnssecdata_get.dsData, self.dnssecExtensionWithMultDsData["dsData"]
dnssecdata_get.dsData, self.dnssecExtensionWithMultDsData.dsData
)
patcher.stop()
def test_user_removes_dnssec_data(self):
"""
Scenario: Registrant removes DNSSEC ds data.
Verify that both the setter and getter are functioning properly
This test verifies:
1 - setter initially calls InfoDomain command
2 - first setter calls UpdateDomain command
3 - second setter calls InfoDomain command again
3 - setter then calls UpdateDomain command
4 - setter adds the UpdateDNSSECExtension extension to the command with rem
"""
# need to use a separate patcher and side_effect for this test, as
# response from InfoDomain must be different for different iterations
# of the same command
def side_effect(_request, cleaned):
if isinstance(_request, commands.InfoDomain):
if mocked_send.call_count == 1:
return MagicMock(res_data=[self.mockDataInfoDomain])
else:
return MagicMock(
res_data=[self.mockDataInfoDomain],
extensions=[self.dnssecExtensionWithDsData],
)
else:
return MagicMock(res_data=[self.mockDataInfoHosts])
patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start()
mocked_send.side_effect = side_effect
domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov")
# dnssecdata_get_initial = domain.dnssecdata # call to force initial mock
# domain._invalidate_cache()
domain.dnssecdata = self.dnssecExtensionWithDsData
domain.dnssecdata = self.dnssecExtensionRemovingDsData
# get the DNS SEC extension added to the UpdateDomain command and
# verify that it is properly sent
# args[0] is the _request sent to registry
args, _ = mocked_send.call_args
# assert that the extension on the update matches
self.assertEquals(
args[0].extensions[0],
self.createUpdateExtension(
self.dnssecExtensionWithDsData,
remove=True,
),
)
mocked_send.assert_has_calls(
[
call(
commands.InfoDomain(
name="dnssec-dsdata.gov",
),
cleaned=True,
),
call(
commands.UpdateDomain(
name="dnssec-dsdata.gov",
nsset=None,
keyset=None,
registrant=None,
auth_info=None,
),
cleaned=True,
),
call(
commands.InfoDomain(
name="dnssec-dsdata.gov",
),
cleaned=True,
),
call(
commands.UpdateDomain(
name="dnssec-dsdata.gov",
nsset=None,
keyset=None,
registrant=None,
auth_info=None,
),
cleaned=True,
),
]
)
patcher.stop()
def test_user_adds_dnssec_keydata(self):
"""
Scenario: Registrant adds DNSSEC data.
Scenario: Registrant adds DNSSEC key data.
Verify that both the setter and getter are functioning properly
This test verifies:
@ -1721,22 +1811,28 @@ class TestRegistrantDNSSEC(MockEppLib):
"""
# make sure to stop any other patcher so there are no conflicts
self.mockSendPatch.stop()
# need to use a separate patcher and side_effect for this test, as
# response from InfoDomain must be different for different iterations
# of the same command
def side_effect(_request, cleaned):
if isinstance(_request, commands.InfoDomain):
if mocked_send.call_count == 1:
return MagicMock(res_data=[self.mockDataInfoDomain])
else:
return MagicMock(
res_data=[self.mockDataInfoDomain],
extensions=[
extensions.DNSSECExtension(**self.dnssecExtensionWithKeyData)
],
extensions=[self.dnssecExtensionWithKeyData],
)
else:
return MagicMock(res_data=[self.mockDataInfoHosts])
patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start()
mocked_send.side_effect = side_effect
self.domain.dnssecdata = self.dnssecExtensionWithKeyData
domain, _ = Domain.objects.get_or_create(name="dnssec-keydata.gov")
domain.dnssecdata = self.dnssecExtensionWithKeyData
# get the DNS SEC extension added to the UpdateDomain command
# and verify that it is properly sent
# args[0] is the _request sent to registry
@ -1744,17 +1840,15 @@ class TestRegistrantDNSSEC(MockEppLib):
# assert that the extension matches
self.assertEquals(
args[0].extensions[0],
self.createUpdateExtension(
extensions.DNSSECExtension(**self.dnssecExtensionWithKeyData)
),
self.createUpdateExtension(self.dnssecExtensionWithKeyData),
)
# test that the dnssecdata getter is functioning properly
dnssecdata_get = self.domain.dnssecdata
dnssecdata_get = domain.dnssecdata
mocked_send.assert_has_calls(
[
call(
commands.UpdateDomain(
name="fake.gov",
name="dnssec-keydata.gov",
nsset=None,
keyset=None,
registrant=None,
@ -1764,7 +1858,7 @@ class TestRegistrantDNSSEC(MockEppLib):
),
call(
commands.InfoDomain(
name="fake.gov",
name="dnssec-keydata.gov",
),
cleaned=True,
),
@ -1772,7 +1866,7 @@ class TestRegistrantDNSSEC(MockEppLib):
)
self.assertEquals(
dnssecdata_get.keyData, self.dnssecExtensionWithKeyData["keyData"]
dnssecdata_get.keyData, self.dnssecExtensionWithKeyData.keyData
)
patcher.stop()
@ -1784,27 +1878,14 @@ class TestRegistrantDNSSEC(MockEppLib):
Then a user-friendly error message is returned for displaying on the web
"""
# make sure to stop any other patcher so there are no conflicts
self.mockSendPatch.stop()
domain, _ = Domain.objects.get_or_create(name="dnssec-invalid.gov")
def side_effect(_request, cleaned):
raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR)
patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start()
mocked_send.side_effect = side_effect
# if RegistryError is raised, view formats user-friendly
# error message if error is_client_error, is_session_error, or
# is_server_error; so test for those conditions
with self.assertRaises(RegistryError) as err:
self.domain.dnssecdata = self.dnssecExtensionWithDsData
domain.dnssecdata = self.dnssecExtensionWithDsData
self.assertTrue(
err.is_client_error() or err.is_session_error() or err.is_server_error()
)
patcher.stop()
class TestAnalystClientHold(MockEppLib):
"""Rule: Analysts may suspend or restore a domain by using client hold"""

View file

@ -18,6 +18,7 @@ from registrar.models import (
DraftDomain,
DomainInvitation,
Contact,
PublicContact,
Website,
UserDomainRole,
User,
@ -1070,21 +1071,60 @@ class TestWithDomainPermissions(TestWithUser):
def setUp(self):
super().setUp()
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
self.domain_dsdata, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov")
self.domain_multdsdata, _ = Domain.objects.get_or_create(
name="dnssec-multdsdata.gov"
)
self.domain_keydata, _ = Domain.objects.get_or_create(name="dnssec-keydata.gov")
# We could simply use domain (igorville) but this will be more readable in tests
# that inherit this setUp
self.domain_dnssec_none, _ = Domain.objects.get_or_create(
name="dnssec-none.gov"
)
self.domain_information, _ = DomainInformation.objects.get_or_create(
creator=self.user, domain=self.domain
)
DomainInformation.objects.get_or_create(
creator=self.user, domain=self.domain_dsdata
)
DomainInformation.objects.get_or_create(
creator=self.user, domain=self.domain_multdsdata
)
DomainInformation.objects.get_or_create(
creator=self.user, domain=self.domain_keydata
)
DomainInformation.objects.get_or_create(
creator=self.user, domain=self.domain_dnssec_none
)
self.role, _ = UserDomainRole.objects.get_or_create(
user=self.user, domain=self.domain, role=UserDomainRole.Roles.ADMIN
)
UserDomainRole.objects.get_or_create(
user=self.user, domain=self.domain_dsdata, role=UserDomainRole.Roles.ADMIN
)
UserDomainRole.objects.get_or_create(
user=self.user,
domain=self.domain_multdsdata,
role=UserDomainRole.Roles.ADMIN,
)
UserDomainRole.objects.get_or_create(
user=self.user, domain=self.domain_keydata, role=UserDomainRole.Roles.ADMIN
)
UserDomainRole.objects.get_or_create(
user=self.user,
domain=self.domain_dnssec_none,
role=UserDomainRole.Roles.ADMIN,
)
def tearDown(self):
try:
self.domain_information.delete()
UserDomainRole.objects.all().delete()
if hasattr(self.domain, "contacts"):
self.domain.contacts.all().delete()
DomainApplication.objects.all().delete()
self.domain.delete()
self.role.delete()
PublicContact.objects.all().delete()
Domain.objects.all().delete()
UserDomainRole.objects.all().delete()
except ValueError: # pass if already deleted
pass
super().tearDown()
@ -1097,7 +1137,7 @@ class TestDomainPermissions(TestWithDomainPermissions):
"domain",
"domain-users",
"domain-users-add",
"domain-nameservers",
"domain-dns-nameservers",
"domain-org-name-address",
"domain-authorizing-official",
"domain-your-contact-information",
@ -1118,7 +1158,7 @@ class TestDomainPermissions(TestWithDomainPermissions):
"domain",
"domain-users",
"domain-users-add",
"domain-nameservers",
"domain-dns-nameservers",
"domain-org-name-address",
"domain-authorizing-official",
"domain-your-contact-information",
@ -1132,7 +1172,7 @@ class TestDomainPermissions(TestWithDomainPermissions):
self.assertEqual(response.status_code, 403)
class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
class TestDomainOverview(TestWithDomainPermissions, WebTest):
def setUp(self):
super().setUp()
self.app.set_user(self.user.username)
@ -1142,10 +1182,24 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
home_page = self.app.get("/")
self.assertContains(home_page, "igorville.gov")
# click the "Edit" link
detail_page = home_page.click("Manage")
detail_page = home_page.click("Manage", index=0)
self.assertContains(detail_page, "igorville.gov")
self.assertContains(detail_page, "Status")
def test_domain_overview_blocked_for_ineligible_user(self):
"""We could easily duplicate this test for all domain management
views, but a single url test should be solid enough since all domain
management pages share the same permissions class"""
self.user.status = User.RESTRICTED
self.user.save()
home_page = self.app.get("/")
self.assertContains(home_page, "igorville.gov")
with less_console_noise():
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
self.assertEqual(response.status_code, 403)
class TestDomainUserManagement(TestDomainOverview):
def test_domain_user_management(self):
response = self.client.get(
reverse("domain-users", kwargs={"pk": self.domain.id})
@ -1304,10 +1358,12 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
home_page = self.app.get(reverse("home"))
self.assertContains(home_page, self.domain.name)
class TestDomainNameservers(TestDomainOverview):
def test_domain_nameservers(self):
"""Can load domain's nameservers page."""
page = self.client.get(
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})
)
self.assertContains(page, "DNS name servers")
@ -1318,7 +1374,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
Uses self.app WebTest because we need to interact with forms.
"""
nameservers_page = self.app.get(
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})
)
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
@ -1328,7 +1384,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
self.assertEqual(result.status_code, 302)
self.assertEqual(
result["Location"],
reverse("domain-nameservers", kwargs={"pk": self.domain.id}),
reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}),
)
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
page = result.follow()
@ -1341,7 +1397,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
Uses self.app WebTest because we need to interact with forms.
"""
nameservers_page = self.app.get(
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})
)
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
@ -1355,6 +1411,8 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
# the field.
self.assertContains(result, "This field is required", count=2, status_code=200)
class TestDomainAuthorizingOfficial(TestDomainOverview):
def test_domain_authorizing_official(self):
"""Can load domain's authorizing official page."""
page = self.client.get(
@ -1373,6 +1431,8 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
)
self.assertContains(page, "Testy")
class TestDomainOrganization(TestDomainOverview):
def test_domain_org_name_address(self):
"""Can load domain's org name and mailing address page."""
page = self.client.get(
@ -1409,6 +1469,8 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
self.assertContains(success_result_page, "Not igorville")
self.assertContains(success_result_page, "Faketown")
class TestDomainContactInformation(TestDomainOverview):
def test_domain_your_contact_information(self):
"""Can load domain's your contact information page."""
page = self.client.get(
@ -1425,6 +1487,8 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
)
self.assertContains(page, "Testy")
class TestDomainSecurityEmail(TestDomainOverview):
def test_domain_security_email_existing_security_contact(self):
"""Can load domain's security email page."""
self.mockSendPatch = patch("registrar.models.domain.registry.send")
@ -1562,6 +1626,214 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib):
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
self.assertEqual(response.status_code, 403)
class TestDomainDNSSEC(TestDomainOverview):
"""MockEPPLib is already inherited."""
def test_dnssec_page_refreshes_enable_button(self):
"""DNSSEC overview page loads when domain has no DNSSEC data
and shows a 'Enable DNSSEC' button. When button is clicked the template
updates. When user navigates away then comes back to the page, the
'Enable DNSSEC' button is shown again."""
# home_page = self.app.get("/")
page = self.client.get(
reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id})
)
self.assertContains(page, "Enable DNSSEC")
# Prepare the data for the POST request
post_data = {
"enable_dnssec": "Enable DNSSEC",
}
updated_page = self.client.post(
reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}),
post_data,
follow=True,
)
self.assertEqual(updated_page.status_code, 200)
self.assertContains(updated_page, "Add DS Data")
self.assertContains(updated_page, "Add Key Data")
self.app.get("/")
back_to_page = self.client.get(
reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id})
)
self.assertContains(back_to_page, "Enable DNSSEC")
def test_dnssec_page_loads_with_data_in_domain(self):
"""DNSSEC overview page loads when domain has DNSSEC data
and the template contains a button to disable DNSSEC."""
page = self.client.get(
reverse("domain-dns-dnssec", kwargs={"pk": self.domain_multdsdata.id})
)
self.assertContains(page, "Disable DNSSEC")
# Prepare the data for the POST request
post_data = {
"disable_dnssec": "Disable DNSSEC",
}
updated_page = self.client.post(
reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}),
post_data,
follow=True,
)
self.assertEqual(updated_page.status_code, 200)
self.assertContains(updated_page, "Enable DNSSEC")
def test_ds_form_loads_with_no_domain_data(self):
"""DNSSEC Add DS Data page loads when there is no
domain DNSSEC data and shows a button to Add new record"""
page = self.client.get(
reverse(
"domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dnssec_none.id}
)
)
self.assertContains(page, "You have no DS Data added")
self.assertContains(page, "Add new record")
def test_ds_form_loads_with_ds_data(self):
"""DNSSEC Add DS Data page loads when there is
domain DNSSEC DS data and shows the data"""
page = self.client.get(
reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})
)
self.assertContains(page, "DS Data record 1")
def test_ds_form_loads_with_key_data(self):
"""DNSSEC Add DS Data page loads when there is
domain DNSSEC KEY data and shows an alert"""
page = self.client.get(
reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_keydata.id})
)
self.assertContains(page, "Warning, you cannot add DS Data")
def test_key_form_loads_with_no_domain_data(self):
"""DNSSEC Add Key Data page loads when there is no
domain DNSSEC data and shows a button to Add DS Key record"""
page = self.client.get(
reverse(
"domain-dns-dnssec-keydata", kwargs={"pk": self.domain_dnssec_none.id}
)
)
self.assertContains(page, "Add DS Key record")
def test_key_form_loads_with_key_data(self):
"""DNSSEC Add Key Data page loads when there is
domain DNSSEC Key data and shows the data"""
page = self.client.get(
reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_keydata.id})
)
self.assertContains(page, "DS Data record 1")
def test_key_form_loads_with_ds_data(self):
"""DNSSEC Add Key Data page loads when there is
domain DNSSEC DS data and shows an alert"""
page = self.client.get(
reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_dsdata.id})
)
self.assertContains(page, "Warning, you cannot add Key Data")
def test_ds_data_form_submits(self):
"""DS Data form submits successfully
Uses self.app WebTest because we need to interact with forms.
"""
add_data_page = self.app.get(
reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})
)
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
with less_console_noise(): # swallow log warning message
result = add_data_page.forms[0].submit()
# form submission was a post, response should be a redirect
self.assertEqual(result.status_code, 302)
self.assertEqual(
result["Location"],
reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}),
)
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
page = result.follow()
self.assertContains(
page, "The DS Data records for this domain have been updated."
)
def test_ds_data_form_invalid(self):
"""DS Data form errors with invalid data
Uses self.app WebTest because we need to interact with forms.
"""
add_data_page = self.app.get(
reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})
)
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# first two nameservers are required, so if we empty one out we should
# get a form error
add_data_page.forms[0]["form-0-key_tag"] = ""
with less_console_noise(): # swallow logged warning message
result = add_data_page.forms[0].submit()
# form submission was a post with an error, response should be a 200
# error text appears twice, once at the top of the page, once around
# the field.
self.assertContains(result, "Key tag is required", count=2, status_code=200)
def test_key_data_form_submits(self):
"""Key Data form submits successfully
Uses self.app WebTest because we need to interact with forms.
"""
add_data_page = self.app.get(
reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_keydata.id})
)
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
with less_console_noise(): # swallow log warning message
result = add_data_page.forms[0].submit()
# form submission was a post, response should be a redirect
self.assertEqual(result.status_code, 302)
self.assertEqual(
result["Location"],
reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_keydata.id}),
)
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
page = result.follow()
self.assertContains(
page, "The Key Data records for this domain have been updated."
)
def test_key_data_form_invalid(self):
"""Key Data form errors with invalid data
Uses self.app WebTest because we need to interact with forms.
"""
add_data_page = self.app.get(
reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_keydata.id})
)
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# first two nameservers are required, so if we empty one out we should
# get a form error
add_data_page.forms[0]["form-0-pub_key"] = ""
with less_console_noise(): # swallow logged warning message
result = add_data_page.forms[0].submit()
# form submission was a post with an error, response should be a 200
# error text appears twice, once at the top of the page, once around
# the field.
self.assertContains(result, "Pub key is required", count=2, status_code=200)
class TestApplicationStatus(TestWithUser, WebTest):
def setUp(self):

View file

@ -3,7 +3,11 @@ from .domain import (
DomainView,
DomainAuthorizingOfficialView,
DomainOrgNameAddressView,
DomainDNSView,
DomainNameserversView,
DomainDNSSECView,
DomainDsDataView,
DomainKeyDataView,
DomainYourContactInformationView,
DomainSecurityEmailView,
DomainUsersView,

View file

@ -11,6 +11,7 @@ from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.db import IntegrityError
from django.shortcuts import redirect
from django.template import RequestContext
from django.urls import reverse
from django.views.generic.edit import FormMixin
@ -30,9 +31,16 @@ from ..forms import (
DomainAddUserForm,
DomainSecurityEmailForm,
NameserverFormset,
DomainDnssecForm,
DomainDsdataFormset,
DomainDsdataForm,
DomainKeydataFormset,
DomainKeydataForm,
)
from epplibwrapper import (
common,
extensions,
RegistryError,
CANNOT_CONTACT_REGISTRY,
GENERIC_ERROR,
@ -46,7 +54,6 @@ logger = logging.getLogger(__name__)
class DomainView(DomainPermissionView):
"""Domain detail overview page."""
template_name = "domain_detail.html"
@ -108,7 +115,6 @@ class DomainOrgNameAddressView(DomainPermissionView, FormMixin):
class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin):
"""Domain authorizing official editing view."""
model = Domain
@ -150,8 +156,13 @@ class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin):
return super().form_valid(form)
class DomainNameserversView(DomainPermissionView, FormMixin):
class DomainDNSView(DomainPermissionView):
"""DNS Information View."""
template_name = "domain_dns.html"
class DomainNameserversView(DomainPermissionView, FormMixin):
"""Domain nameserver editing view."""
template_name = "domain_nameservers.html"
@ -175,7 +186,7 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
def get_success_url(self):
"""Redirect to the nameservers page for the domain."""
return reverse("domain-nameservers", kwargs={"pk": self.object.pk})
return reverse("domain-dns-nameservers", kwargs={"pk": self.object.pk})
def get_context_data(self, **kwargs):
"""Adjust context from FormMixin for formsets."""
@ -229,8 +240,301 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
return super().form_valid(formset)
class DomainYourContactInformationView(DomainPermissionView, FormMixin):
class DomainDNSSECView(DomainPermissionView, FormMixin):
"""Domain DNSSEC editing view."""
template_name = "domain_dnssec.html"
form_class = DomainDnssecForm
def get_context_data(self, **kwargs):
"""The initial value for the form (which is a formset here)."""
context = super().get_context_data(**kwargs)
self.domain = self.get_object()
has_dnssec_records = self.domain.dnssecdata is not None
# Create HTML for the modal button
modal_button = (
'<button type="submit" '
'class="usa-button" '
'name="disable_dnssec">Disable DNSSEC</button>'
)
context["modal_button"] = modal_button
context["has_dnssec_records"] = has_dnssec_records
context["dnssec_enabled"] = self.request.session.pop("dnssec_enabled", False)
return context
def get_success_url(self):
"""Redirect to the DNSSEC page for the domain."""
return reverse("domain-dns-dnssec", kwargs={"pk": self.domain.pk})
def post(self, request, *args, **kwargs):
"""Form submission posts to this view."""
self.domain = self.get_object()
form = self.get_form()
if form.is_valid():
if "disable_dnssec" in request.POST:
try:
self.domain.dnssecdata = {}
except RegistryError as err:
errmsg = "Error removing existing DNSSEC record(s)."
logger.error(errmsg + ": " + err)
messages.error(self.request, errmsg)
request.session["dnssec_ds_confirmed"] = False
request.session["dnssec_key_confirmed"] = False
elif "enable_dnssec" in request.POST:
request.session["dnssec_enabled"] = True
request.session["dnssec_ds_confirmed"] = False
request.session["dnssec_key_confirmed"] = False
return self.form_valid(form)
class DomainDsDataView(DomainPermissionView, FormMixin):
"""Domain DNSSEC ds data editing view."""
template_name = "domain_dsdata.html"
form_class = DomainDsdataFormset
form = DomainDsdataForm
def get_initial(self):
"""The initial value for the form (which is a formset here)."""
domain = self.get_object()
dnssecdata: extensions.DNSSECExtension = domain.dnssecdata
initial_data = []
if dnssecdata is not None:
if dnssecdata.keyData is not None:
# TODO: Throw an error
# Note: This is moot if we're
# removing key data
pass
if dnssecdata.dsData is not None:
# Add existing nameservers as initial data
initial_data.extend(
{
"key_tag": record.keyTag,
"algorithm": record.alg,
"digest_type": record.digestType,
"digest": record.digest,
}
for record in dnssecdata.dsData
)
# Ensure at least 1 record, filled or empty
while len(initial_data) == 0:
initial_data.append({})
return initial_data
def get_success_url(self):
"""Redirect to the DS Data page for the domain."""
return reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.object.pk})
def get_context_data(self, **kwargs):
"""Adjust context from FormMixin for formsets."""
context = super().get_context_data(**kwargs)
# use "formset" instead of "form" for the key
context["formset"] = context.pop("form")
# set the dnssec_ds_confirmed flag in the context for this view
# based either on the existence of DS Data in the domain,
# or on the flag stored in the session
domain = self.get_object()
dnssecdata: extensions.DNSSECExtension = domain.dnssecdata
if dnssecdata is not None and dnssecdata.dsData is not None:
self.request.session["dnssec_ds_confirmed"] = True
context["dnssec_ds_confirmed"] = self.request.session.get(
"dnssec_ds_confirmed", False
)
return context
def post(self, request, *args, **kwargs):
"""Formset submission posts to this view."""
self.object = self.get_object()
formset = self.get_form()
if "confirm-ds" in request.POST:
request.session["dnssec_ds_confirmed"] = True
request.session["dnssec_key_confirmed"] = False
return super().form_valid(formset)
if "btn-cancel-click" in request.POST:
return redirect("/", {"formset": formset}, RequestContext(request))
if formset.is_valid():
return self.form_valid(formset)
else:
return self.form_invalid(formset)
def form_valid(self, formset):
"""The formset is valid, perform something with it."""
# Set the dnssecdata from the formset
dnssecdata = extensions.DNSSECExtension()
for form in formset:
try:
# if 'delete' not in form.cleaned_data
# or form.cleaned_data['delete'] == False:
dsrecord = {
"keyTag": form.cleaned_data["key_tag"],
"alg": int(form.cleaned_data["algorithm"]),
"digestType": int(form.cleaned_data["digest_type"]),
"digest": form.cleaned_data["digest"],
}
if dnssecdata.dsData is None:
dnssecdata.dsData = []
dnssecdata.dsData.append(common.DSData(**dsrecord))
except KeyError:
# no cleaned_data provided for this form, but passed
# as valid; this can happen if form has been added but
# not been interacted with; in that case, want to ignore
pass
domain = self.get_object()
try:
domain.dnssecdata = dnssecdata
except RegistryError as err:
errmsg = "Error updating DNSSEC data in the registry."
logger.error(errmsg)
logger.error(err)
messages.error(self.request, errmsg)
return self.form_invalid(formset)
else:
messages.success(
self.request, "The DS Data records for this domain have been updated."
)
# superclass has the redirect
return super().form_valid(formset)
class DomainKeyDataView(DomainPermissionView, FormMixin):
"""Domain DNSSEC key data editing view."""
template_name = "domain_keydata.html"
form_class = DomainKeydataFormset
form = DomainKeydataForm
def get_initial(self):
"""The initial value for the form (which is a formset here)."""
domain = self.get_object()
dnssecdata: extensions.DNSSECExtension = domain.dnssecdata
initial_data = []
if dnssecdata is not None:
if dnssecdata.dsData is not None:
# TODO: Throw an error?
# Note: this is moot if we're
# removing Key data
pass
if dnssecdata.keyData is not None:
# Add existing keydata as initial data
initial_data.extend(
{
"flag": record.flags,
"protocol": record.protocol,
"algorithm": record.alg,
"pub_key": record.pubKey,
}
for record in dnssecdata.keyData
)
# Ensure at least 1 record, filled or empty
while len(initial_data) == 0:
initial_data.append({})
return initial_data
def get_success_url(self):
"""Redirect to the Key Data page for the domain."""
return reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.object.pk})
def get_context_data(self, **kwargs):
"""Adjust context from FormMixin for formsets."""
context = super().get_context_data(**kwargs)
# use "formset" instead of "form" for the key
context["formset"] = context.pop("form")
# set the dnssec_key_confirmed flag in the context for this view
# based either on the existence of Key Data in the domain,
# or on the flag stored in the session
domain = self.get_object()
dnssecdata: extensions.DNSSECExtension = domain.dnssecdata
if dnssecdata is not None and dnssecdata.keyData is not None:
self.request.session["dnssec_key_confirmed"] = True
context["dnssec_key_confirmed"] = self.request.session.get(
"dnssec_key_confirmed", False
)
return context
def post(self, request, *args, **kwargs):
"""Formset submission posts to this view."""
self.object = self.get_object()
formset = self.get_form()
if "confirm-key" in request.POST:
request.session["dnssec_key_confirmed"] = True
request.session["dnssec_ds_confirmed"] = False
self.object.save()
return super().form_valid(formset)
if "btn-cancel-click" in request.POST:
return redirect("/", {"formset": formset}, RequestContext(request))
if formset.is_valid():
return self.form_valid(formset)
else:
return self.form_invalid(formset)
def form_valid(self, formset):
"""The formset is valid, perform something with it."""
# Set the nameservers from the formset
dnssecdata = extensions.DNSSECExtension()
for form in formset:
try:
# if 'delete' not in form.cleaned_data
# or form.cleaned_data['delete'] == False:
keyrecord = {
"flags": int(form.cleaned_data["flag"]),
"protocol": int(form.cleaned_data["protocol"]),
"alg": int(form.cleaned_data["algorithm"]),
"pubKey": form.cleaned_data["pub_key"],
}
if dnssecdata.keyData is None:
dnssecdata.keyData = []
dnssecdata.keyData.append(common.DNSSECKeyData(**keyrecord))
except KeyError:
# no server information in this field, skip it
pass
domain = self.get_object()
try:
domain.dnssecdata = dnssecdata
except RegistryError as err:
errmsg = "Error updating DNSSEC data in the registry."
logger.error(errmsg)
logger.error(err)
messages.error(self.request, errmsg)
return self.form_invalid(formset)
else:
messages.success(
self.request, "The Key Data records for this domain have been updated."
)
# superclass has the redirect
return super().form_valid(formset)
class DomainYourContactInformationView(DomainPermissionView, FormMixin):
"""Domain your contact information editing view."""
template_name = "domain_your_contact_information.html"
@ -271,7 +575,6 @@ class DomainYourContactInformationView(DomainPermissionView, FormMixin):
class DomainSecurityEmailView(DomainPermissionView, FormMixin):
"""Domain security email editing view."""
template_name = "domain_security_email.html"
@ -345,14 +648,12 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin):
class DomainUsersView(DomainPermissionView):
"""User management page in the domain details."""
template_name = "domain_users.html"
class DomainAddUserView(DomainPermissionView, FormMixin):
"""Inside of a domain's user management, a form for adding users.
Multiple inheritance is used here for permissions, form handling, and