diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 204e5ca92..bd5b22da7 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -83,6 +83,11 @@ urlpatterns = [ views.DomainNameserversView.as_view(), name="domain-nameservers", ), + path( + "domain//securityemail", + views.DomainSecurityEmailView.as_view(), + name="domain-security-email", + ), path( "domain//users/add", views.DomainAddUserView.as_view(), diff --git a/src/registrar/forms/__init__.py b/src/registrar/forms/__init__.py index 6c1b5d8cf..6cfe07818 100644 --- a/src/registrar/forms/__init__.py +++ b/src/registrar/forms/__init__.py @@ -1,2 +1,2 @@ from .application_wizard import * -from .domain import DomainAddUserForm, NameserverFormset +from .domain import DomainAddUserForm, NameserverFormset, DomainSecurityEmailForm diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index cca0bf5c9..83e13b685 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -22,3 +22,10 @@ NameserverFormset = formset_factory( DomainNameserverForm, extra=1, ) + + +class DomainSecurityEmailForm(forms.Form): + + """Form for adding or editing a security email to a domain.""" + + security_email = forms.EmailField(label="Security email") diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 7d287595a..22aa41751 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -235,6 +235,19 @@ class Domain(TimeStampedModel): # nothing. logger.warn("TODO: Fake setting nameservers to %s", new_nameservers) + def security_email(self) -> str: + """Get the security email for this domain. + + TODO: call EPP to get this info instead of returning fake data. + """ + return "mayor@igorville.gov" + + def set_security_email(self, new_security_email: str): + """Set the security email for this domain.""" + # TODO: call EPP to set these values in the registry instead of doing + # nothing. + logger.warn("TODO: Fake setting security email to %s", new_security_email) + @property def roid(self): return self._get_property("roid") diff --git a/src/registrar/templates/domain_security_email.html b/src/registrar/templates/domain_security_email.html new file mode 100644 index 000000000..0e2a663b8 --- /dev/null +++ b/src/registrar/templates/domain_security_email.html @@ -0,0 +1,27 @@ +{% extends "domain_base.html" %} +{% load static field_helpers %} + +{% block title %}Domain security email | {{ domain.name }} | {% endblock %} + +{% block domain_content %} + +

Domain security email

+ +

We strongly recommend that you provide a security email. This email will allow the public to report observed or suspected security issues on your domain. Security emails are made public and included in the .gov domain data we provide.

+ +

A security contact should be capable of evaluating or triaging security reports for your entire domain. Use a team email address, not an individual’s email. We recommend using an alias, like security@domain.gov.

+ + {% include "includes/required_fields.html" %} + +
+ {% csrf_token %} + + {% input_with_errors form.security_email %} + + +
+ +{% endblock %} {# domain_content #} diff --git a/src/registrar/templates/domain_sidebar.html b/src/registrar/templates/domain_sidebar.html index 8114f1be6..ebfe160fa 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -40,7 +40,7 @@
  • - {% url 'todo' as url %} + {% url 'domain-security-email' pk=domain.id as url %} diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index f1dfec73a..ee83034e1 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1063,6 +1063,11 @@ class TestDomainPermissions(TestWithDomainPermissions): ) self.assertEqual(response.status_code, 302) + response = self.client.get( + reverse("domain-security-email", kwargs={"pk": self.domain.id}) + ) + self.assertEqual(response.status_code, 302) + def test_no_domain_role(self): """Logged in but no role gets 403 Forbidden.""" self.client.force_login(self.user) @@ -1282,6 +1287,38 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest): # the field. self.assertContains(result, "This field is required", count=2, status_code=200) + def test_domain_security_email(self): + """Can load domain's security email page.""" + page = self.client.get( + reverse("domain-security-email", kwargs={"pk": self.domain.id}) + ) + self.assertContains(page, "Domain security email") + + def test_domain_security_email_form(self): + """Adding a security email works. + + Uses self.app WebTest because we need to interact with forms. + """ + security_email_page = self.app.get( + reverse("domain-security-email", kwargs={"pk": self.domain.id}) + ) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + security_email_page.form["security_email"] = "mayor@igorville.gov" + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + with less_console_noise(): # swallow log warning message + result = security_email_page.form.submit() + self.assertEqual(result.status_code, 302) + self.assertEqual( + result["Location"], + reverse("domain-security-email", kwargs={"pk": self.domain.id}), + ) + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + success_page = result.follow() + self.assertContains( + success_page, "The security email for this domain have been updated" + ) + class TestApplicationStatus(TestWithUser, WebTest): def setUp(self): diff --git a/src/registrar/views/__init__.py b/src/registrar/views/__init__.py index 9f7fe139e..820d0295d 100644 --- a/src/registrar/views/__init__.py +++ b/src/registrar/views/__init__.py @@ -2,6 +2,7 @@ from .application import * from .domain import ( DomainView, DomainNameserversView, + DomainSecurityEmailView, DomainUsersView, DomainAddUserView, DomainInvitationDeleteView, diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 8a9095de3..6242496d1 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -12,7 +12,7 @@ from django.views.generic.edit import DeleteView, FormMixin from registrar.models import Domain, DomainInvitation, User, UserDomainRole -from ..forms import DomainAddUserForm, NameserverFormset +from ..forms import DomainAddUserForm, NameserverFormset, DomainSecurityEmailForm from ..utility.email import send_templated_email, EmailSendingError from .utility import DomainPermission @@ -96,6 +96,55 @@ class DomainNameserversView(DomainPermission, FormMixin, DetailView): return super().form_valid(formset) +class DomainSecurityEmailView(DomainPermission, FormMixin, DetailView): + + """Domain security email editing view.""" + + model = Domain + template_name = "domain_security_email.html" + context_object_name = "domain" + form_class = DomainSecurityEmailForm + + def get_initial(self): + """The initial value for the form.""" + domain = self.get_object() + initial = super().get_initial() + initial["security_email"] = domain.security_email() + return initial + + def get_success_url(self): + """Redirect to the overview page for the domain.""" + return reverse("domain-security-email", kwargs={"pk": self.object.pk}) + + def post(self, request, *args, **kwargs): + """Formset submission posts to this view.""" + self.object = self.get_object() + form = self.get_form() + if form.is_valid(): + # there is a valid email address in the form + return self.form_valid(form) + else: + return self.form_invalid(form) + + def form_valid(self, form): + """The form is valid, call setter in model.""" + + # Set the security email from the from + try: + new_email = form.cleaned_data["security_email"] + except KeyError: + # no server information in this field, skip it + pass + domain = self.get_object() + domain.set_security_email(new_email) + + messages.success( + self.request, "The security email for this domain have been updated" + ) + # superclass has the redirect + return redirect(self.get_success_url()) + + class DomainUsersView(DomainPermission, DetailView): """User management page in the domain details.""" diff --git a/src/zap.conf b/src/zap.conf index 3658bfe6c..bb86d60b7 100644 --- a/src/zap.conf +++ b/src/zap.conf @@ -52,6 +52,7 @@ 10038 OUTOFSCOPE http://app:8080/users 10038 OUTOFSCOPE http://app:8080/users/add 10038 OUTOFSCOPE http://app:8080/nameservers +10038 OUTOFSCOPE http://app:8080/securityemail 10038 OUTOFSCOPE http://app:8080/delete 10038 OUTOFSCOPE http://app:8080/withdraw 10038 OUTOFSCOPE http://app:8080/withdrawconfirmed