From c01a0276e501926b9c5e6404a1202c2c93b17dcc Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Wed, 17 May 2023 10:39:49 -0400 Subject: [PATCH 01/11] Implement domain security email page with stubs for EPP --- src/registrar/config/urls.py | 5 ++ src/registrar/forms/__init__.py | 2 +- src/registrar/forms/domain.py | 7 +++ src/registrar/models/domain.py | 13 +++++ .../templates/domain_security_email.html | 27 ++++++++++ src/registrar/templates/domain_sidebar.html | 2 +- src/registrar/tests/test_views.py | 37 ++++++++++++++ src/registrar/views/__init__.py | 1 + src/registrar/views/domain.py | 51 ++++++++++++++++++- src/zap.conf | 1 + 10 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 src/registrar/templates/domain_security_email.html 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 From 6a86a53d04d77794ed93e28077081e0344f56f8a Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Wed, 17 May 2023 13:39:46 -0400 Subject: [PATCH 02/11] remove security email from domainInfo, create migration, simplify form_valid set security email --- ...20_remove_domaininformation_security_email.py | 16 ++++++++++++++++ src/registrar/models/domain_information.py | 6 ------ src/registrar/views/domain.py | 12 ++++-------- 3 files changed, 20 insertions(+), 14 deletions(-) create mode 100644 src/registrar/migrations/0020_remove_domaininformation_security_email.py diff --git a/src/registrar/migrations/0020_remove_domaininformation_security_email.py b/src/registrar/migrations/0020_remove_domaininformation_security_email.py new file mode 100644 index 000000000..9742c294a --- /dev/null +++ b/src/registrar/migrations/0020_remove_domaininformation_security_email.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2 on 2023-05-17 17:05 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0019_alter_domainapplication_organization_type"), + ] + + operations = [ + migrations.RemoveField( + model_name="domaininformation", + name="security_email", + ), + ] diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 6561a82b4..c7832266b 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -200,12 +200,6 @@ class DomainInformation(TimeStampedModel): blank=True, help_text="Acknowledged .gov acceptable use policy", ) - security_email = models.EmailField( - max_length=320, - null=True, - blank=True, - help_text="Security email for public use", - ) def __str__(self): try: diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 6242496d1..d0fa8952e 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -90,7 +90,7 @@ class DomainNameserversView(DomainPermission, FormMixin, DetailView): domain.set_nameservers(nameservers) messages.success( - self.request, "The name servers for this domain have been updated" + self.request, "The name servers for this domain have been updated." ) # superclass has the redirect return super().form_valid(formset) @@ -129,17 +129,13 @@ class DomainSecurityEmailView(DomainPermission, FormMixin, DetailView): 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 + # Set the security email from the form + new_email = form.cleaned_data["security_email"] domain = self.get_object() domain.set_security_email(new_email) messages.success( - self.request, "The security email for this domain have been updated" + self.request, "The security email for this domain have been updated." ) # superclass has the redirect return redirect(self.get_success_url()) From f60f2bff050bfd60aff3448db4d6b09e471bc4cc Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Wed, 17 May 2023 18:41:52 -0400 Subject: [PATCH 03/11] Document local migrations, mention --wait for cf migrations --- docs/developer/database-access.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/developer/database-access.md b/docs/developer/database-access.md index 8fc5d4617..8c0fc612e 100644 --- a/docs/developer/database-access.md +++ b/docs/developer/database-access.md @@ -12,19 +12,32 @@ to get a `psql` shell on the sandbox environment's database. ## Running Migrations +### On Local + +```shell +docker-compose exec app bash +./manage.py makemigrations +``` + +The docker compose down then up to run the new migrations. + +### On Cloud.gov + When new code changes the database schema, we need to apply Django's migrations. We can run these using CloudFoundry's tasks to run the `manage.py migrate` command in the correct environment. For any developer environment, developers can manually run the task with ```shell -cf run-task getgov-ENVIRONMENT --command 'python manage.py migrate' --name migrate +cf run-task getgov-ENVIRONMENT --wait --command 'python manage.py migrate' --name migrate ``` +(The optional 'wait' argument will wait until the environment is stable) + Optionally, load data from fixtures as well ```shell -cf run-task getgov-ENVIRONMENT --command 'python manage.py load' --name loaddata +cf run-task getgov-ENVIRONMENT --wait --command 'python manage.py load' --name loaddata ``` For the `stable` environment, developers don't have credentials so we need to From 45dfa9734f9cc56204b192178eb5f64b2ef27c1f Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Wed, 17 May 2023 20:42:12 -0400 Subject: [PATCH 04/11] Add back tery/except and add a default value to catch all possible error states --- src/registrar/views/domain.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index d0fa8952e..da469462e 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -130,7 +130,11 @@ class DomainSecurityEmailView(DomainPermission, FormMixin, DetailView): """The form is valid, call setter in model.""" # Set the security email from the form - new_email = form.cleaned_data["security_email"] + new_email = "" + try: + new_email = form.cleaned_data["security_email"] + except: + pass domain = self.get_object() domain.set_security_email(new_email) From c086d4c7b75cc1e97f92c817e5be68debcf71489 Mon Sep 17 00:00:00 2001 From: rachidatecs <107004823+rachidatecs@users.noreply.github.com> Date: Wed, 17 May 2023 20:45:21 -0400 Subject: [PATCH 05/11] Update docs/developer/database-access.md Co-authored-by: Alysia Broddrick <109625347+abroddrick@users.noreply.github.com> --- docs/developer/database-access.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer/database-access.md b/docs/developer/database-access.md index 8c0fc612e..2fc05013b 100644 --- a/docs/developer/database-access.md +++ b/docs/developer/database-access.md @@ -19,7 +19,7 @@ docker-compose exec app bash ./manage.py makemigrations ``` -The docker compose down then up to run the new migrations. +Then perform docker-compose down & docker-compose up to run with the new migrations. ### On Cloud.gov From 4f96fd65a7d995be281c478e10627f9645b79672 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Wed, 17 May 2023 20:49:58 -0400 Subject: [PATCH 06/11] Add context for migrations documentation --- docs/developer/database-access.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/developer/database-access.md b/docs/developer/database-access.md index 8c0fc612e..7c7d28881 100644 --- a/docs/developer/database-access.md +++ b/docs/developer/database-access.md @@ -12,6 +12,9 @@ to get a `psql` shell on the sandbox environment's database. ## Running Migrations +When new code changes the database schema (ie, you change a model or pull some +code that has), we need to apply Django's migrations. + ### On Local ```shell @@ -23,7 +26,6 @@ The docker compose down then up to run the new migrations. ### On Cloud.gov -When new code changes the database schema, we need to apply Django's migrations. We can run these using CloudFoundry's tasks to run the `manage.py migrate` command in the correct environment. For any developer environment, developers can manually run the task with From 1436eb12bd5317985fce58a97ac902d9ba09ee0a Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Wed, 17 May 2023 21:00:50 -0400 Subject: [PATCH 07/11] Specify expection type KeyError --- src/registrar/views/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index da469462e..b13a7ab43 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -133,7 +133,7 @@ class DomainSecurityEmailView(DomainPermission, FormMixin, DetailView): new_email = "" try: new_email = form.cleaned_data["security_email"] - except: + except KeyError: pass domain = self.get_object() domain.set_security_email(new_email) From c07d858b187f55aa6b608f4834268054bcc4e9f1 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Thu, 18 May 2023 17:09:19 -0400 Subject: [PATCH 08/11] remove unecessary try/catch --- src/registrar/views/domain.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index b13a7ab43..d0fa8952e 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -130,11 +130,7 @@ class DomainSecurityEmailView(DomainPermission, FormMixin, DetailView): """The form is valid, call setter in model.""" # Set the security email from the form - new_email = "" - try: - new_email = form.cleaned_data["security_email"] - except KeyError: - pass + new_email = form.cleaned_data["security_email"] domain = self.get_object() domain.set_security_email(new_email) From a95ff2bf53fc5e3c088a9556ec1e958ff76d1ca8 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Fri, 19 May 2023 10:48:34 -0400 Subject: [PATCH 09/11] add test --- src/registrar/tests/test_views.py | 6 ++++++ src/registrar/views/domain.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index ee83034e1..959183f34 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1095,6 +1095,12 @@ class TestDomainPermissions(TestWithDomainPermissions): ) self.assertEqual(response.status_code, 403) + with less_console_noise(): + response = self.client.get( + reverse("domain-security-email", kwargs={"pk": self.domain.id}) + ) + self.assertEqual(response.status_code, 403) + class TestDomainDetail(TestWithDomainPermissions, WebTest): def setUp(self): diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index d0fa8952e..b5962c398 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -130,7 +130,7 @@ class DomainSecurityEmailView(DomainPermission, FormMixin, DetailView): """The form is valid, call setter in model.""" # Set the security email from the form - new_email = form.cleaned_data["security_email"] + new_email = form.cleaned_data.get("security_email", "") domain = self.get_object() domain.set_security_email(new_email) From a85bd5231f9b0719602858ae15d471da13f9f5f1 Mon Sep 17 00:00:00 2001 From: rachidatecs Date: Fri, 19 May 2023 10:59:55 -0400 Subject: [PATCH 10/11] change the url from securityemail to security-email --- src/registrar/config/urls.py | 2 +- src/zap.conf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index bd5b22da7..e903b4e8f 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -84,7 +84,7 @@ urlpatterns = [ name="domain-nameservers", ), path( - "domain//securityemail", + "domain//security-email", views.DomainSecurityEmailView.as_view(), name="domain-security-email", ), diff --git a/src/zap.conf b/src/zap.conf index bb86d60b7..ee92e8a1c 100644 --- a/src/zap.conf +++ b/src/zap.conf @@ -52,7 +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/security-email 10038 OUTOFSCOPE http://app:8080/delete 10038 OUTOFSCOPE http://app:8080/withdraw 10038 OUTOFSCOPE http://app:8080/withdrawconfirmed From ace9a8e6a790849ba5ba944a0591f1023fd039c0 Mon Sep 17 00:00:00 2001 From: Michelle Rago <60157596+michelle-rago@users.noreply.github.com> Date: Mon, 22 May 2023 12:01:34 -0400 Subject: [PATCH 11/11] Update banner text (#635) * Update banner text * Update base.html * Added
    code back --- src/registrar/templates/base.html | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/registrar/templates/base.html b/src/registrar/templates/base.html index f90ac2014..3ca280a0b 100644 --- a/src/registrar/templates/base.html +++ b/src/registrar/templates/base.html @@ -61,18 +61,15 @@ Skip to main content {% if IS_DEMO_SITE %} -
    -
    -
    -

    - TEST SITE - Do not use real personal information. Demo purposes only. -

    -
    +
    +
    +
    +

    + BETA SITE: We’re building a new way to get a .gov. Take a look around, but don’t rely on this site yet. This site is for testing purposes only. Don’t enter real data into any form on this site. To learn about requesting a .gov domain, visit get.gov +

    -
    +
    +
    {% endif %}