Merge branch 'main' into nmb/519-authorizing-official

This commit is contained in:
Neil Martinsen-Burrell 2023-05-22 12:33:33 -05:00
commit 4fdc1ae2cc
No known key found for this signature in database
GPG key ID: 6A3C818CC10D0184
14 changed files with 198 additions and 24 deletions

View file

@ -12,19 +12,34 @@ to get a `psql` shell on the sandbox environment's database.
## Running Migrations ## 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` We can run these using CloudFoundry's tasks to run the `manage.py migrate`
command in the correct environment. For any developer environment, developers command in the correct environment. For any developer environment, developers
can manually run the task with can manually run the task with
```shell ```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 Optionally, load data from fixtures as well
```shell ```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 For the `stable` environment, developers don't have credentials so we need to

View file

@ -88,6 +88,11 @@ urlpatterns = [
views.DomainAuthorizingOfficialView.as_view(), views.DomainAuthorizingOfficialView.as_view(),
name="domain-authorizing-official", name="domain-authorizing-official",
), ),
path(
"domain/<int:pk>/security-email",
views.DomainSecurityEmailView.as_view(),
name="domain-security-email",
),
path( path(
"domain/<int:pk>/users/add", "domain/<int:pk>/users/add",
views.DomainAddUserView.as_view(), views.DomainAddUserView.as_view(),

View file

@ -1,2 +1,7 @@
from .application_wizard import * from .application_wizard import *
from .domain import DomainAddUserForm, NameserverFormset, ContactForm from .domain import (
DomainAddUserForm,
NameserverFormset,
DomainSecurityEmailForm,
ContactForm,
)

View file

@ -62,3 +62,10 @@ class ContactForm(forms.ModelForm):
for field_name in self.required: for field_name in self.required:
self.fields[field_name].required = True 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")

View file

@ -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",
),
]

View file

@ -235,6 +235,19 @@ class Domain(TimeStampedModel):
# nothing. # nothing.
logger.warn("TODO: Fake setting nameservers to %s", new_nameservers) 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 @property
def roid(self): def roid(self):
return self._get_property("roid") return self._get_property("roid")

View file

@ -200,12 +200,6 @@ class DomainInformation(TimeStampedModel):
blank=True, blank=True,
help_text="Acknowledged .gov acceptable use policy", 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): def __str__(self):
try: try:

View file

@ -61,18 +61,15 @@
<a class="usa-skipnav" href="#main-content">Skip to main content</a> <a class="usa-skipnav" href="#main-content">Skip to main content</a>
{% if IS_DEMO_SITE %} {% if IS_DEMO_SITE %}
<section <section aria-label="Alert" >
class="usa-site-alert usa-site-alert--emergency usa-site-alert--no-icon" <div class="usa-alert usa-alert--warning usa-alert--no-icon">
aria-label="Site alert" <div class="usa-alert__body">
> <p class="usa-alert__text">
<div class="usa-alert"> <strong>BETA SITE:</strong> Were building a new way to get a .gov. Take a look around, but dont rely on this site yet. This site is for testing purposes only. Dont enter real data into any form on this site. To learn about requesting a .gov domain, visit <a href="https://get.gov" class="usa-link">get.gov</a>
<div class="usa-alert__body"> </p>
<p class="usa-alert__text">
<strong>TEST SITE</strong> - Do not use real personal information. Demo purposes only.
</p>
</div>
</div> </div>
</section> </div>
</section>
{% endif %} {% endif %}
<section class="usa-banner" aria-label="Official website of the United States government"> <section class="usa-banner" aria-label="Official website of the United States government">

View file

@ -0,0 +1,27 @@
{% extends "domain_base.html" %}
{% load static field_helpers %}
{% block title %}Domain security email | {{ domain.name }} | {% endblock %}
{% block domain_content %}
<h1>Domain security email</h1>
<p>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 <a href="https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/about/data/">.gov domain data</a> we provide.</p>
<p>A security contact should be capable of evaluating or triaging security reports for your entire domain. Use a team email address, not an individuals email. We recommend using an alias, like security@domain.gov.</p>
{% include "includes/required_fields.html" %}
<form class="usa-form usa-form--large" method="post" novalidate>
{% csrf_token %}
{% input_with_errors form.security_email %}
<button
type="submit"
class="usa-button"
>Add security email</button>
</form>
{% endblock %} {# domain_content #}

View file

@ -49,7 +49,7 @@
</li> </li>
<li class="usa-sidenav__item"> <li class="usa-sidenav__item">
{% url 'todo' as url %} {% url 'domain-security-email' pk=domain.id as url %}
<a href="{{ url }}" <a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %} {% if request.path == url %}class="usa-current"{% endif %}
> >

View file

@ -1063,6 +1063,11 @@ class TestDomainPermissions(TestWithDomainPermissions):
) )
self.assertEqual(response.status_code, 302) 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): def test_no_domain_role(self):
"""Logged in but no role gets 403 Forbidden.""" """Logged in but no role gets 403 Forbidden."""
self.client.force_login(self.user) self.client.force_login(self.user)
@ -1082,6 +1087,12 @@ class TestDomainPermissions(TestWithDomainPermissions):
) )
self.assertEqual(response.status_code, 403) 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): class TestDomainDetail(TestWithDomainPermissions, WebTest):
def setUp(self): def setUp(self):
@ -1292,6 +1303,38 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
) )
self.assertContains(page, "Testy") 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): class TestApplicationStatus(TestWithUser, WebTest):
def setUp(self): def setUp(self):

View file

@ -3,6 +3,7 @@ from .domain import (
DomainView, DomainView,
DomainAuthorizingOfficialView, DomainAuthorizingOfficialView,
DomainNameserversView, DomainNameserversView,
DomainSecurityEmailView,
DomainUsersView, DomainUsersView,
DomainAddUserView, DomainAddUserView,
DomainInvitationDeleteView, DomainInvitationDeleteView,

View file

@ -12,7 +12,12 @@ from django.views.generic.edit import DeleteView, FormMixin
from registrar.models import Domain, DomainInvitation, User, UserDomainRole 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.email import send_templated_email, EmailSendingError
from .utility import DomainPermission from .utility import DomainPermission
@ -133,12 +138,57 @@ class DomainNameserversView(DomainPermission, FormMixin, DetailView):
domain.set_nameservers(nameservers) domain.set_nameservers(nameservers)
messages.success( 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 # superclass has the redirect
return super().form_valid(formset) 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): class DomainUsersView(DomainPermission, DetailView):
"""User management page in the domain details.""" """User management page in the domain details."""

View file

@ -52,6 +52,7 @@
10038 OUTOFSCOPE http://app:8080/users 10038 OUTOFSCOPE http://app:8080/users
10038 OUTOFSCOPE http://app:8080/users/add 10038 OUTOFSCOPE http://app:8080/users/add
10038 OUTOFSCOPE http://app:8080/nameservers 10038 OUTOFSCOPE http://app:8080/nameservers
10038 OUTOFSCOPE http://app:8080/security-email
10038 OUTOFSCOPE http://app:8080/delete 10038 OUTOFSCOPE http://app:8080/delete
10038 OUTOFSCOPE http://app:8080/withdraw 10038 OUTOFSCOPE http://app:8080/withdraw
10038 OUTOFSCOPE http://app:8080/withdrawconfirmed 10038 OUTOFSCOPE http://app:8080/withdrawconfirmed