diff --git a/docs/developer/database-access.md b/docs/developer/database-access.md index 8fc5d4617..9d615c477 100644 --- a/docs/developer/database-access.md +++ b/docs/developer/database-access.md @@ -12,19 +12,34 @@ to get a `psql` shell on the sandbox environment's database. ## Running Migrations -When new code changes the database schema, we need to apply Django's 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 +docker-compose exec app bash +./manage.py makemigrations +``` + +Then perform docker-compose down & docker-compose up to run with the new migrations. + +### On Cloud.gov + 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 diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 1087a2f19..1bc0bf4c1 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -88,6 +88,11 @@ urlpatterns = [ views.DomainAuthorizingOfficialView.as_view(), name="domain-authorizing-official", ), + path( + "domain//security-email", + 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 8e35825aa..364740211 100644 --- a/src/registrar/forms/__init__.py +++ b/src/registrar/forms/__init__.py @@ -1,2 +1,7 @@ from .application_wizard import * -from .domain import DomainAddUserForm, NameserverFormset, ContactForm +from .domain import ( + DomainAddUserForm, + NameserverFormset, + DomainSecurityEmailForm, + ContactForm, +) diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index 1c8033a2b..cff8f8edd 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -62,3 +62,10 @@ class ContactForm(forms.ModelForm): for field_name in self.required: self.fields[field_name].required = True + + +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/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.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/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/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 %}
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 ab259a4a7..aa1f14300 100644 --- a/src/registrar/templates/domain_sidebar.html +++ b/src/registrar/templates/domain_sidebar.html @@ -49,7 +49,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 976984f35..dfa26e60f 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) @@ -1082,6 +1087,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): @@ -1292,6 +1303,38 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest): ) self.assertContains(page, "Testy") + 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 2eae93370..1193df1d4 100644 --- a/src/registrar/views/__init__.py +++ b/src/registrar/views/__init__.py @@ -3,6 +3,7 @@ from .domain import ( DomainView, DomainAuthorizingOfficialView, DomainNameserversView, + DomainSecurityEmailView, DomainUsersView, DomainAddUserView, DomainInvitationDeleteView, diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 8b9186958..5cf050319 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -12,7 +12,12 @@ from django.views.generic.edit import DeleteView, FormMixin from registrar.models import Domain, DomainInvitation, User, UserDomainRole -from ..forms import DomainAddUserForm, NameserverFormset, ContactForm +from ..forms import ( + DomainAddUserForm, + NameserverFormset, + DomainSecurityEmailForm, + ContactForm, +) from ..utility.email import send_templated_email, EmailSendingError from .utility import DomainPermission @@ -133,12 +138,57 @@ 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) +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 form + new_email = form.cleaned_data.get("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." + ) + # 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..ee92e8a1c 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/security-email 10038 OUTOFSCOPE http://app:8080/delete 10038 OUTOFSCOPE http://app:8080/withdraw 10038 OUTOFSCOPE http://app:8080/withdrawconfirmed