Merge remote-tracking branch 'origin/main' into rjm/472-email-domain-in-review

This commit is contained in:
rachidatecs 2023-06-12 17:31:28 -04:00
commit 76b0a72ad4
No known key found for this signature in database
GPG key ID: 3CEBBFA7325E5525
41 changed files with 684 additions and 93 deletions

View file

@ -1,6 +1,5 @@
name: Bug
description: Report a bug
title: "[Bug]: "
description: Report a bug or problem with the application
labels: ["bug"]
body:
@ -56,4 +55,14 @@ body:
id: additional-context
attributes:
label: Additional Context (optional)
description: "Please include additional references, screenshots, documentation, etc. that are relevant"
description: "Please include additional references (screenshots, design links, documentation, etc.) that are relevant"
- type: textarea
id: issue-links
attributes:
label: Issue Links (optional)
description: |
What other issues does this story relate to and how?
Example:
- 🚧 Blocked by: #123
- 🔄 Relates to: #234

View file

@ -1,6 +1,5 @@
name: User Story
name: Story
description: Capture actionable sprint work
title: "[Story]: "
labels: ["story"]
body:
@ -13,13 +12,13 @@ body:
- type: textarea
id: story
attributes:
label: User Story
label: Story
description: |
Please add the "as a, I want, so that" details that describe the user story.
Please add the "as a, I want, so that" details that describe the story.
If more than one "as a, I want, so that" describes the story, add multiple.
Example:
As an administrator
As an analyst
I want the ability to approve a domain application
so that a request can be fulfilled and a new .gov domain can be provisioned
value: |
@ -33,23 +32,23 @@ body:
attributes:
label: Acceptance Criteria
description: |
Please add the acceptance criteria using one or more "given, when, then" formulae
Please add the acceptance criteria that best describe the desired outcomes when this work is completed
Example:
Given that I am an administrator who has finished reviewing a domain application
- Application sends an email when analysts approve domain requests
- Domain application status is "approved"
Example ("given, when, then" format):
Given that I am an analyst who has finished reviewing a domain application
When I click to approve a domain application
Then the domain provisioning process should be initiated, and the applicant should receive an email update.
value: |
Given
When
Then
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional Context (optional)
description: "Please include additional references, screenshots, documentation, etc. that are relevant"
description: "Please include additional references (screenshots, design links, documentation, etc.) that are relevant"
- type: textarea
id: issue-links
attributes:

View file

@ -28,7 +28,7 @@ jobs:
docker compose run node npx gulp compile
- name: Collect static assets
working-directory: ./src
run: docker compose run app python manage.py collectstatic
run: docker compose run app python manage.py collectstatic --no-input
- name: Deploy to cloud.gov sandbox
uses: 18f/cg-deploy-action@main
env:

View file

@ -72,8 +72,8 @@ jobs:
# by adding MockUserLogin to settings.MIDDLEWARE
run: |
perl -pi \
-e 's/"csp.middleware.CSPMiddleware",/$&"registrar.tests.common.MockUserLogin",/' \
src/registrar/config/settings.py
-e 's/"django.contrib.auth.middleware.AuthenticationMiddleware",/$&"registrar.tests.common.MockUserLogin",/' \
registrar/config/settings.py
working-directory: ./src
- name: OWASP scan

View file

@ -59,7 +59,7 @@ jobs:
# by adding MockUserLogin to settings.MIDDLEWARE
run: |
perl -pi \
-e 's/"csp.middleware.CSPMiddleware",/$&"registrar.tests.common.MockUserLogin",/' \
-e 's/"django.contrib.auth.middleware.AuthenticationMiddleware",/$&"registrar.tests.common.MockUserLogin",/' \
registrar/config/settings.py
- name: Start container

View file

@ -1,5 +1,4 @@
# Get (your very own) .gov
========================
Welcome to the repo for a WIP brand new registrar for .gov domains. Get.gov intends to serve all government entities in the United States looking for a .gov domain to use publicly (for a website, for an email address, etc.). Here you can find the code for the registrar and other artifacts about our product strategy and research.

View file

@ -199,6 +199,8 @@ Static files (images, CSS stylesheets, JavaScripts, etc) are known as "assets".
Assets are stored in `registrar/assets` during development and served from `registrar/public`. During deployment, assets are copied from `registrar/assets` into `registrar/public`. Any assets which need processing, such as USWDS Sass files, are processed before copying.
**Note:** Custom images are added to `/registrar/assets/img/registrar`, keeping them separate from the images copied over by USWDS. However, because the `/img/` directory is listed in `.gitignore`, any files added to `/registrar/assets/img/registrar` will need to be force added (i.e. `git add --force <img-file>`) before they can be deployed.
We utilize the [uswds-compile tool](https://designsystem.digital.gov/documentation/getting-started/developers/phase-two-compile/) from USWDS to compile and package USWDS assets.
## Making and view style changes

View file

@ -0,0 +1,105 @@
# Working with the registry via EPP
## Overview of parts
**EPP** is the protocol which describes how a registry and registrar communicate with XML over a TCP socket connection.
**epplib** is a Python library implementation of the TCP socket connection. It has helper functions and dataclasses which can be used to send and receive the XML messages.
**epplibwrapper** is a module in this repository which abstracts away the details of authenticating with the registry. It assists with error handling by providing error code constants and an error class with some helper methods.
**Domain** is a Python class. It inherits from `django.db.models.Model` and is therefore part of Django's ORM and has a corresponding table in the local registrar database. Its purpose is to provide a developer-friendly interface to the registry based on *what a registrant or analyst wants to do*, not on the technical details of EPP.
## Debugging in a Python shell
You'll first need access to a Django shell in an environment with valid registry credentials. Only some environments are allowed access: your laptop is probably not one of them. For example:
```shell
cf ssh getgov-ENVIRONMENT
/tmp/lifecycle/shell # this configures your environment
./manage.py shell
```
You'll next need to import some code.
```
from epplibwrapper import CLIENT as registry, commands
from epplib.models import common
```
Finally, you'll need to craft a request and send it.
```
request = ...
response = registry.send(request)
```
Note that you'll need to attest that the data you are sending has been sanitized to remove malicious or invalid strings. Use `send(..., cleaned=True)` to do that.
See below for some example commands to send. Replace example data with data which makes sense for your debugging scenario. Other commands are available; see the source code of epplib for more options.
### Get info about a contact
```
request = commands.InfoContact(id='sh8013')
```
### Create a new contact
```
DF = common.DiscloseField
di = common.Disclose(flag=False, fields={DF.FAX, DF.VOICE, DF.ADDR}, types={DF.ADDR: "loc"})
addr = common.ContactAddr(street=['123 Example Dr.'], city='Dulles', pc='20166-6503', cc='US', sp='VA')
pi = common.PostalInfo(name='John Doe', addr=addr, org="Example Inc.", type="loc")
ai = common.ContactAuthInfo(pw='feedabee')
request = commands.CreateContact(id='sh8013', postal_info=pi, email='jdoe@example.com', voice='+1.7035555555', fax='+1.7035555556', auth_info=ai, disclose=di, vat=None, ident=None, notify_email=None)
```
### Create a new domain
```
ai = common.DomainAuthInfo(pw='feedabee')
request = commands.CreateDomain(name="okay.gov", registrant="sh8013", auth_info=ai)
```
### Create a host object
```
request = commands.CreateHost(name="ns1.okay.gov", addrs=[common.Ip(addr="127.0.0.1"), common.Ip(addr="0:0:0:0:0:0:0:1", ip="v6")])
```
### Check if a host is available
```
request = commands.CheckHost(["ns2.okay.gov"])
```
### Update a domain
```
request = commands.UpdateDomain(name="okay.gov", add=[common.HostObjSet(["ns1.okay.gov"])])
```
```
request = commands.UpdateDomain(name="okay.gov", add=[common.DomainContact(contact="sh8014", type="tech")])
```
### How to see the raw XML
To see the XML of a command before the request is sent, call `request.xml()`.
To see the XML of the response, you must send the command using a different method.
```
registry._client.connect()
registry._client.send(registry._login)
request = commands.InfoDomain(name="ok.gov")
registry._client.transport.send(request.xml())
response = registry._client.transport.receive()
```
This is helpful for debugging situations where epplib is not correctly or fully parsing the XML returned from the registry.

View file

@ -22,12 +22,22 @@ role or set of permissions that they have. We use a `UserDomainRole`
## Permission decorator
The Django objects that need to be permission controlled are various views.
For that purpose, we add a very simple permission mixin
[`DomainPermission`](../../src/registrar/views/utility/mixins.py) that can be
added to a view to require that (a) there is a logged-in user and (b) that the
logged in user has a role that permits access to that view. This mixin is the
place where the details of the permissions are enforced. It can allow a view
to load, or deny access with various status codes, e.g. "403 Forbidden".
For that purpose, we have a View subclass to enforce user permissions on a
domain called
[`DomainPermissionView`](../../src/registrar/views/utility/permission_views.py)
that can be added to a view to require that (a) there is a logged-in user and
(b) that the logged in user has a role that permits access to that view. This
mixin is the place where the details of the permissions are enforced. It can
allow a view to load, or deny access with various status codes, e.g. "403
Forbidden".
In addition, we now require all of our application views to have a logged-in
user by using a Django middleware that makes every request "login required".
This is slightly belt-and-suspenders because our permissions view also checks
that the request includes a logged in user, but it avoids accidentally creating
content that is publicly available by accident. We can specifically mark a view
as "not login required" if we do need to have publicly accessible content (such
as health checks used by our platform).
## Adding roles

View file

@ -24,9 +24,9 @@ django-fsm = "*"
django-phonenumber-field = {extras = ["phonenumberslite"], version = "*"}
boto3 = "*"
typing-extensions ='*'
django-login-required-middleware = "*"
fred-epplib = {git = "https://github.com/cisagov/epplib.git", ref = "master"}
[dev-packages]
django-debug-toolbar = "*"
nplusone = "*"

35
src/Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "fd7d0efa9a87dfe4b2bb228ee0e7978fba16c7cfdd3c443870900cfe899e2cfd"
"sha256": "1242c67b31261243a35128410d4a928fca3729ddac13b8c8e25adf31445c6328"
},
"pipfile-spec": 6,
"requires": {},
@ -306,6 +306,13 @@
"index": "pypi",
"version": "==2.8.1"
},
"django-login-required-middleware": {
"hashes": [
"sha256:847ae9a69fd7a07618ed53192b3c06946af70a0caf6d0f4eb40a8f37593cd970"
],
"index": "pypi",
"version": "==0.9.0"
},
"django-phonenumber-field": {
"extras": [
"phonenumberslite"
@ -791,11 +798,11 @@
},
"typing-extensions": {
"hashes": [
"sha256:06006244c70ac8ee83fa8282cb188f697b8db25bc8b4df07be1873c43897060c",
"sha256:3a8b36f13dd5fdc5d1b16fe317f5668545de77fa0b8e02006381fd49d731ab98"
"sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26",
"sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5"
],
"index": "pypi",
"version": "==4.6.2"
"version": "==4.6.3"
},
"urllib3": {
"hashes": [
@ -951,19 +958,19 @@
},
"django-stubs": {
"hashes": [
"sha256:93baff824f0a056e71036b423b942a74f07b909e45e3fa38185b910f597c5c08",
"sha256:d2c671989efb3f7b0fa91e461909ad5a5a52155fe7fe6d1f2058cb88e3afb123"
"sha256:66477bdba25407623f4079205e58f3c7265a4f0d8f7c9f540a6edc16f8883a5b",
"sha256:8c15d5f7b05926805cfb25f2bfbf3509c37792fbd8aec5aedea358b85d8bccd5"
],
"index": "pypi",
"version": "==4.2.0"
"version": "==4.2.1"
},
"django-stubs-ext": {
"hashes": [
"sha256:55b2e3077f883e0131a7596f8ff8b19f8fc3ca325a3318ccacf5331acb2601e4",
"sha256:7789f0caeca7152fef07ad6b94dec7310a05d0b8dab77f7979e19db0037b5127"
"sha256:2696d6f7d8538341b060cffa9565c72ea797e866687e040b86d29cad8799e5fe",
"sha256:4b6b63e49f4ba30d93ec46f87507648c99c9de6911e651ad69db7084fd5b2f4e"
],
"markers": "python_version >= '3.7'",
"version": "==4.2.0"
"markers": "python_version >= '3.8'",
"version": "==4.2.1"
},
"django-webtest": {
"hashes": [
@ -1306,11 +1313,11 @@
},
"typing-extensions": {
"hashes": [
"sha256:06006244c70ac8ee83fa8282cb188f697b8db25bc8b4df07be1873c43897060c",
"sha256:3a8b36f13dd5fdc5d1b16fe317f5668545de77fa0b8e02006381fd49d731ab98"
"sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26",
"sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5"
],
"index": "pypi",
"version": "==4.6.2"
"version": "==4.6.3"
},
"urllib3": {
"hashes": [

View file

@ -104,6 +104,9 @@ class AvailableAPITest(TestCase):
def test_available_post(self):
"""Cannot post to the /available/ API endpoint."""
# have to log in to test the correct thing now that we require login
# for all URLs by default
self.client.force_login(self.user)
with less_console_noise():
response = self.client.post(API_BASE_PATH + "nonsense")
self.assertEqual(response.status_code, 405)

View file

@ -124,7 +124,6 @@ h2 {
}
/* Make "placeholder" links visually obvious */
a[href^="https://federalist-"]::after,
a[href$="todo"]::after {
background-color: yellow;
color: color(blue-80v);

View file

@ -48,6 +48,7 @@ env_db_url = env.dj_db_url("DATABASE_URL")
env_debug = env.bool("DJANGO_DEBUG", default=False)
env_log_level = env.str("DJANGO_LOG_LEVEL", "DEBUG")
env_base_url = env.str("DJANGO_BASE_URL")
env_getgov_public_site_url = env.str("GETGOV_PUBLIC_SITE_URL", "")
secret_login_key = b64decode(secret("DJANGO_SECRET_LOGIN_KEY", ""))
secret_key = secret("DJANGO_SECRET_KEY")
@ -62,8 +63,6 @@ secret_registry_key = b64decode(secret("REGISTRY_KEY", ""))
secret_registry_key_passphrase = secret("REGISTRY_KEY_PASSPHRASE", "")
secret_registry_hostname = secret("REGISTRY_HOSTNAME")
secret_getgov_public_site_url = secret("GETGOV_PUBLIC_SITE_URL", "")
# region: Basic Django Config-----------------------------------------------###
# Build paths inside the project like this: BASE_DIR / "subdir".
@ -134,6 +133,8 @@ MIDDLEWARE = [
"django.middleware.csrf.CsrfViewMiddleware",
# add `user` (the currently-logged-in user) to incoming HttpRequest objects
"django.contrib.auth.middleware.AuthenticationMiddleware",
# Require login for every single request by default
"login_required.middleware.LoginRequiredMiddleware",
# provide framework for displaying messages to the user, see documentation
"django.contrib.messages.middleware.MessageMiddleware",
# provide clickjacking protection via the X-Frame-Options header
@ -461,6 +462,12 @@ AUTHENTICATION_BACKENDS = [
# the login_required() decorator, LoginRequiredMixin, or AccessMixin
LOGIN_URL = "/openid/login"
# We don't want the OIDC app to be login-required because then it can't handle
# the initial login requests without erroring.
LOGIN_REQUIRED_IGNORE_PATHS = [
r"/openid/(.+)$",
]
# where to go after logging out
LOGOUT_REDIRECT_URL = "home"
@ -509,7 +516,7 @@ STATIC_URL = "public/"
# Base URL of our separate static public website. Used by the
# {% public_site_url subdir/path %} template tag
GETGOV_PUBLIC_SITE_URL = secret_getgov_public_site_url
GETGOV_PUBLIC_SITE_URL = env_getgov_public_site_url
# endregion
# region: Registry----------------------------------------------------------###

View file

@ -88,6 +88,11 @@ urlpatterns = [
views.DomainYourContactInformationView.as_view(),
name="domain-your-contact-information",
),
path(
"domain/<int:pk>/org-name-address",
views.DomainOrgNameAddressView.as_view(),
name="domain-org-name-address",
),
path(
"domain/<int:pk>/authorizing-official",
views.DomainAuthorizingOfficialView.as_view(),

View file

@ -59,6 +59,21 @@ class UserFixture:
"first_name": "Alysia",
"last_name": "Broddrick",
},
{
"username": "55a3bc26-cd1d-4a5c-a8c0-7e1f561ef7f4",
"first_name": "Michelle",
"last_name": "Rago",
},
{
"username": "8f8e7293-17f7-4716-889b-1990241cbd39",
"first_name": "Katherine",
"last_name": "Osos",
},
{
"username": "70488e0a-e937-4894-a28c-16f5949effd4",
"first_name": "Gaby",
"last_name": "DiSarli",
},
]
@classmethod

View file

@ -3,5 +3,6 @@ from .domain import (
DomainAddUserForm,
NameserverFormset,
DomainSecurityEmailForm,
DomainOrgNameAddressForm,
ContactForm,
)

View file

@ -1,11 +1,12 @@
"""Forms for domain management."""
from django import forms
from django.core.validators import RegexValidator
from django.forms import formset_factory
from phonenumber_field.widgets import RegionalPhoneNumberWidget
from ..models import Contact
from ..models import Contact, DomainInformation
class DomainAddUserForm(forms.Form):
@ -64,3 +65,77 @@ class DomainSecurityEmailForm(forms.Form):
"""Form for adding or editing a security email to a domain."""
security_email = forms.EmailField(label="Security email")
class DomainOrgNameAddressForm(forms.ModelForm):
"""Form for updating the organization name and mailing address."""
zipcode = forms.CharField(
label="Zip code",
validators=[
RegexValidator(
"^[0-9]{5}(?:-[0-9]{4})?$|^$",
message="Enter a zip code in the form of 12345 or 12345-6789.",
)
],
)
class Meta:
model = DomainInformation
fields = [
"federal_agency",
"organization_name",
"address_line1",
"address_line2",
"city",
"state_territory",
"zipcode",
"urbanization",
]
error_messages = {
"federal_agency": {
"required": "Select the federal agency for your organization."
},
"organization_name": {"required": "Enter the name of your organization."},
"address_line1": {
"required": "Enter the street address of your organization."
},
"city": {"required": "Enter the city where your organization is located."},
"state_territory": {
"required": "Select the state, territory, or military post where your"
"organization is located."
},
}
widgets = {
# We need to set the required attributed for federal_agency and
# state/territory because for these fields we are creating an individual
# instance of the Select. For the other fields we use the for loop to set
# the class's required attribute to true.
"federal_agency": forms.Select(
attrs={"required": True}, choices=DomainInformation.AGENCY_CHOICES
),
"organization_name": forms.TextInput,
"address_line1": forms.TextInput,
"address_line2": forms.TextInput,
"city": forms.TextInput,
"state_territory": forms.Select(
attrs={
"required": True,
},
choices=DomainInformation.StateTerritoryChoices.choices,
),
"urbanization": forms.TextInput,
}
# the database fields have blank=True so ModelForm doesn't create
# required fields by default. Use this list in __init__ to mark each
# of these fields as required
required = ["organization_name", "address_line1", "city", "zipcode"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field_name in self.required:
self.fields[field_name].required = True
self.fields["state_territory"].widget.attrs.pop("maxlength", None)
self.fields["zipcode"].widget.attrs.pop("maxlength", None)

View file

@ -0,0 +1,53 @@
# Generated by Django 4.2.1 on 2023-06-09 16:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0026_alter_domainapplication_address_line2_and_more"),
]
operations = [
migrations.AlterField(
model_name="domaininformation",
name="address_line1",
field=models.TextField(
blank=True,
help_text="Street address",
null=True,
verbose_name="Street address",
),
),
migrations.AlterField(
model_name="domaininformation",
name="address_line2",
field=models.TextField(
blank=True,
help_text="Street address line 2",
null=True,
verbose_name="Street address line 2",
),
),
migrations.AlterField(
model_name="domaininformation",
name="state_territory",
field=models.CharField(
blank=True,
help_text="State, territory, or military post",
max_length=2,
null=True,
verbose_name="State, territory, or military post",
),
),
migrations.AlterField(
model_name="domaininformation",
name="urbanization",
field=models.TextField(
blank=True,
help_text="Urbanization (Puerto Rico only)",
null=True,
verbose_name="Urbanization (Puerto Rico only)",
),
),
]

View file

@ -577,6 +577,10 @@ class DomainApplication(TimeStampedModel):
# When an application is moved to in review, we need to send a
# confirmation email. This is a side-effect of the state transition
updated_domain_application._send_in_review_email()
@transition(field="status", source=[SUBMITTED, INVESTIGATING], target=WITHDRAWN)
def withdraw(self):
"""Withdraw an application that has been submitted."""
# ## Form policies ###
#

View file

@ -100,11 +100,13 @@ class DomainInformation(TimeStampedModel):
null=True,
blank=True,
help_text="Street address",
verbose_name="Street address",
)
address_line2 = models.TextField(
null=True,
blank=True,
help_text="Street address line 2",
verbose_name="Street address line 2",
)
city = models.TextField(
null=True,
@ -116,6 +118,7 @@ class DomainInformation(TimeStampedModel):
null=True,
blank=True,
help_text="State, territory, or military post",
verbose_name="State, territory, or military post",
)
zipcode = models.CharField(
max_length=10,
@ -128,6 +131,7 @@ class DomainInformation(TimeStampedModel):
null=True,
blank=True,
help_text="Urbanization (Puerto Rico only)",
verbose_name="Urbanization (Puerto Rico only)",
)
type_of_work = models.TextField(

View file

@ -1,5 +1,6 @@
{% extends "base.html" %}
{% load i18n static %}
{% load url_helpers %}
{% block title %}{% translate "Unauthorized | " %}{% endblock %}
@ -25,7 +26,7 @@
Would you like to <a href="{% url 'login' %}"> try logging in again?</a>
</p>
<p>
If you would like help with this error <a href="https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/contact/"> contact us </a>
If you'd like help with this error <a href="{% public_site_url 'contact/' %}"> contact us </a>.
</p>
{% if log_identifier %}

View file

@ -1,5 +1,6 @@
{% extends "base.html" %}
{% load i18n static %}
{% load url_helpers %}
{% block title %}{% translate "Forbidden | " %}{% endblock %}
@ -22,12 +23,12 @@
{% endif %}
<p>
You must be an authorized user and need to be signed in to view this page.
Would you like to <a href="{% url 'login' %}"> try logging in again?</a>
Would you like to <a href="{% url 'login' %}"> try logging in again</a>?
</p>
<p>
If you would like help with this error <a href="https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/contact/"> contact us </a>
If you'd like help with this error <a href="{% public_site_url 'contact' %}"> contact us </a>.
</p>
{% if log_identifier %}
<p>Here's a unique identifier for this error.</p>
<p class="text-semibold">{{ log_identifier }}</p>

View file

@ -1,5 +1,6 @@
{% extends "base.html" %}
{% load i18n static %}
{% load url_helpers %}
{% block title %}{% translate "Page not found | " %}{% endblock %}
@ -14,7 +15,7 @@
{% translate "Status 404" %}
</h2>
<p> Try going to the <a href="/">homepage</a>. If you cant find what youre looking for, <a href= "https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/contact/">contact us.</a>
<p> Try going to the <a href="/">homepage</a>. If you cant find what youre looking for, <a href="{% public_site_url 'contact' %}"> contact us </a>.
</p>
</div>

View file

@ -1,5 +1,6 @@
{% extends "base.html" %}
{% load i18n static %}
{% load url_helpers %}
{% block title %}{% translate "Server error | " %}{% endblock %}
@ -18,7 +19,7 @@
{% else %}
<p>
Sorry! Try waiting a few minutes and then reloading the page.
<a href="https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/contact/"> Contact us </a> if you need help.
<a href="{% public_site_url 'contact' %}"> contact us </a> if you need help.
</p>
{% endif %}

View file

@ -1,22 +1,19 @@
{% extends 'application_form.html' %}
{% load field_helpers %}
{% load field_helpers url_helpers %}
{% block form_instructions %}
<h2 class="margin-bottom-05">
Who is the authorizing official for your organization?
</h2>
<p>Your authorizing official is the person within your organization who can authorize
your domain request. This is generally the highest-ranking or highest-elected official
in your organization. Read more about <a href="{% url 'todo' %}">who can serve as an
authorizing official</a>.</p>
<p>Your authorizing official is the person within your organization who can authorize your domain request. This is generally the highest-ranking or highest-elected official in your organization.</p>
<div class="ao_example">
{% include "includes/ao_example.html" %}
</div>
<p>Well contact your authorizing official to let them know that you made this request
and to double check that they approve it.</p>
<p>We might contact your authorizing official, or their office, to double check that they approve this request. Read more about <a href="{% public_site_url 'domains/eligibility/#you-must-have-approval-from-an-authorizing-official-within-your-organization' %}">who can serve as an authorizing official</a>.</p>
{% endblock %}

View file

@ -1,9 +1,9 @@
{% extends 'application_form.html' %}
{% load static field_helpers %}
{% load static field_helpers url_helpers %}
{% block form_instructions %}
<p>Before requesting a .gov domain, <a href="{% url 'todo' %}">please make sure it
meets our naming requirements.</a> Your domain name must:
<p>Before requesting a .gov domain, <a href="{% public_site_url 'domains/choosing' %}">please make sure it
meets our naming requirements</a>. Your domain name must:
<ul class="usa-list">
<li>Be available </li>
<li>Be unique </li>

View file

@ -1,5 +1,5 @@
{% extends "domain_base.html" %}
{% load static field_helpers%}
{% load static field_helpers url_helpers %}
{% block title %}Domain authorizing official | {{ domain.name }} | {% endblock %}
@ -11,9 +11,7 @@
<p>Your authorizing official is the person within your organization who can
authorize domain requests. This is generally the highest-ranking or
highest-elected official in your organization. <a class="usa-link"
href="https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/domains/eligibility/#you-must-have-approval-from-an-authorizing-official-within-your-organization">Read more about who can serve
as an authorizing official.</a></p>
highest-elected official in your organization. Read more about <a class="usa-link" href="{% public_site_url 'domains/eligibility/#you-must-have-approval-from-an-authorizing-official-within-your-organization' %}">who can serve as an authorizing official</a>.</p>
{% include "includes/required_fields.html" %}

View file

@ -14,7 +14,7 @@
<a class="usa-button margin-bottom-1" href="{{url}}"> Add DNS name servers </a>
{% endif %}
{% url 'todo' as url %}
{% url 'domain-org-name-address' pk=domain.id as url %}
{% include "includes/summary_item.html" with title='Organization name and mailing address' value=domain.domain_info address='true' edit_link=url %}
{% url 'domain-authorizing-official' pk=domain.id as url %}

View file

@ -0,0 +1,47 @@
{% extends "domain_base.html" %}
{% load static field_helpers%}
{% block title %}Organization name and mailing address | {{ domain.name }} | {% endblock %}
{% block domain_content %}
{# this is right after the messages block in the parent template #}
{% include "includes/form_errors.html" with form=form %}
<h1>Organization name and mailing address </h1>
<p>The name of your organization will be publicly listed as the domain registrant.</p>
{% include "includes/required_fields.html" %}
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
{% csrf_token %}
{% if domain.domain_info.organization_type == 'federal' %}
{% input_with_errors form.federal_agency %}
{% endif %}
{% input_with_errors form.organization_name %}
{% input_with_errors form.address_line1 %}
{% input_with_errors form.address_line2 %}
{% input_with_errors form.city %}
{% input_with_errors form.state_territory %}
{% with add_class="usa-input--small" %}
{% input_with_errors form.zipcode %}
{% endwith %}
{% input_with_errors form.urbanization %}
<button
type="submit"
class="usa-button"
>
Save
</button>
</form>
{% endblock %} {# domain_content #}

View file

@ -1,5 +1,5 @@
{% extends "domain_base.html" %}
{% load static field_helpers %}
{% load static field_helpers url_helpers %}
{% block title %}Domain security email | {{ domain.name }} | {% endblock %}
@ -7,7 +7,7 @@
<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>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="{% public_site_url '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>

View file

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

View file

@ -1,4 +1,5 @@
{% load static %}
{% load url_helpers %}
<footer class="usa-footer">
<div class="usa-footer__secondary-section">
@ -26,11 +27,11 @@
<address class="usa-footer__address">
<div class="usa-footer__contact-info grid-row grid-gap-md">
<div class="grid-col-auto">
<a href="https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/help/"> Help </a>
<a href="{% public_site_url 'help/' %}" class="usa-link"> Help </a>
</div>
<span class=""> | </span>
<div class="grid-col-auto">
<a href="https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/contact/" class="usa-link">Contact us</a>
<a href="{% public_site_url 'contact/' %}" class="usa-link">Contact us</a>
</div>
</div>
</address>
@ -73,11 +74,8 @@
<div class="usa-identifier__container">
<ul class="usa-identifier__required-links-list">
<li class="usa-identifier__required-links-item">
<a
href="https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/about/"
class="usa-identifier__required-link usa-link"
>About .gov</a
>
<a href="{% public_site_url 'about/' %}"
class="usa-identifier__required-link usa-link">About .gov</a>
</li>
<li class="usa-identifier__required-links-item">
<a
@ -87,9 +85,7 @@
>
</li>
<li class="usa-identifier__required-links-item">
<a href="https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/privacy-policy/" class="usa-identifier__required-link usa-link"
>Privacy policy</a
>
<a href="{% public_site_url 'privacy-policy/' %}" class="usa-identifier__required-link usa-link">Privacy policy</a>
</li>
<li class="usa-identifier__required-links-item">
<a href="https://www.dhs.gov/accessibility" class="usa-identifier__required-link usa-link usa-link--external"
@ -97,9 +93,8 @@
>
</li>
<li class="usa-identifier__required-links-item">
<a href="https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/vulnerability-disclosure-policy/" class="usa-identifier__required-link usa-link"
>Vulnerability disclosure policy</a
>
<a href="{% public_site_url 'vulnerability-disclosure-policy/' %}" class="usa-identifier__required-link usa-link"
>Vulnerability disclosure policy</a>
</li>
<li class="usa-identifier__required-links-item">
<a href="https://www.cisa.gov/cisa-no-fear-act-reporting" class="usa-identifier__required-link usa-link"

View file

@ -0,0 +1,159 @@
"""Test that almost all URLs require authentication.
This uses deep Django URLConf pattern magic and was shamelessly lifted from
https://github.com/18F/tock/blob/main/tock/tock/tests/test_url_auth.py
"""
from django.test import TestCase
from django.urls import reverse, URLPattern
from django.urls.resolvers import URLResolver
import registrar.config.urls
from .common import less_console_noise
# When a URLconf pattern contains named capture groups, we'll use this
# dictionary to retrieve a sample value for it, which will be included
# in the sample URLs we generate, when attempting to perform a GET
# request on the view.
SAMPLE_KWARGS = {
"app_label": "registrar",
"pk": "1",
"id": "1",
"content_type_id": "2",
"object_id": "3",
"domain": "whitehouse.gov",
}
# Our test suite will ignore some namespaces.
IGNORE_NAMESPACES = [
# The Django Debug Toolbar (DJDT) ends up in the URL config but it's always
# disabled in production, so don't worry about it.
"djdt"
]
# In general, we don't want to have any unnamed views, because that makes it
# impossible to generate sample URLs that point at them. We'll make exceptions
# for some namespaces that we don't have control over, though.
NAMESPACES_WITH_UNNAMED_VIEWS = ["admin", None]
def iter_patterns(urlconf, patterns=None, namespace=None):
"""
Iterate through all patterns in the given Django URLconf. Yields
`(viewname, route)` tuples, where `viewname` is the fully-qualified view name
(including its namespace, if any), and `route` is a regular expression that
corresponds to the part of the pattern that contains any capturing groups.
"""
if patterns is None:
patterns = urlconf.urlpatterns
for pattern in patterns:
# Resolve if it's a route or an include
if isinstance(pattern, URLPattern):
viewname = pattern.name
if viewname is None and namespace not in NAMESPACES_WITH_UNNAMED_VIEWS:
raise AssertionError(
f"namespace {namespace} cannot contain unnamed views"
)
if namespace and viewname is not None:
viewname = f"{namespace}:{viewname}"
yield (viewname, pattern.pattern)
elif isinstance(pattern, URLResolver):
if len(pattern.default_kwargs.keys()) > 0:
raise AssertionError("resolvers are not expected to have kwargs")
if pattern.namespace and namespace is not None:
raise AssertionError("nested namespaces are not currently supported")
if pattern.namespace in IGNORE_NAMESPACES:
continue
yield from iter_patterns(
urlconf, pattern.url_patterns, namespace or pattern.namespace
)
else:
raise AssertionError("unknown pattern class")
def iter_sample_urls(urlconf):
"""
Yields sample URLs for all entries in the given Django URLconf.
This gets pretty deep into the muck of RoutePattern
https://docs.djangoproject.com/en/2.1/_modules/django/urls/resolvers/
"""
for viewname, route in iter_patterns(urlconf):
if not viewname:
continue
if viewname == "auth_user_password_change":
print(route)
break
named_groups = route.regex.groupindex.keys()
kwargs = {}
args = ()
for kwarg in named_groups:
if kwarg not in SAMPLE_KWARGS:
raise AssertionError(
f'Sample value for {kwarg} in pattern "{route}" not found'
)
kwargs[kwarg] = SAMPLE_KWARGS[kwarg]
url = reverse(viewname, args=args, kwargs=kwargs)
yield (viewname, url)
class TestURLAuth(TestCase):
"""
Tests to ensure that most URLs in a Django URLconf are protected by
authentication.
"""
# We won't test that the following URLs are protected by auth.
# Note that the trailing slash is wobbly depending on how the URL was defined.
IGNORE_URLS = [
# These are the OIDC auth endpoints that always need
# to be public.
"/openid/login/",
"/openid/logout/",
"/openid/callback",
"/openid/callback/login/",
"/openid/callback/logout/",
]
def assertURLIsProtectedByAuth(self, url):
"""
Make a GET request to the given URL, and ensure that it either redirects
to login or denies access outright.
"""
try:
with less_console_noise():
response = self.client.get(url)
except Exception as e:
# It'll be helpful to provide information on what URL was being
# accessed at the time the exception occurred. Python 3 will
# also include a full traceback of the original exception, so
# we don't need to worry about hiding the original cause.
raise AssertionError(f'Accessing {url} raised "{e}"', e)
code = response.status_code
if code == 302:
redirect = response["location"]
self.assertRegex(
redirect,
r"^\/openid\/login",
f"GET {url} should redirect to login or deny access, but instead "
f"it redirects to {redirect}",
)
elif code == 401 or code == 403:
pass
else:
raise AssertionError(
f"GET {url} returned HTTP {code}, but should redirect to login or "
"deny access",
)
def test_login_required_all_urls(self):
"""All URLs redirect to the login view."""
for viewname, url in iter_sample_urls(registrar.config.urls):
if url not in self.IGNORE_URLS:
with self.subTest(viewname=viewname):
self.assertURLIsProtectedByAuth(url)

View file

@ -36,10 +36,9 @@ class TestViews(TestCase):
self.assertContains(response, "OK", status_code=200)
def test_home_page(self):
"""Home page should be available without a login."""
"""Home page should NOT be available without a login."""
response = self.client.get("/")
self.assertContains(response, "registrar", status_code=200)
self.assertContains(response, "Sign in")
self.assertEqual(response.status_code, 302)
def test_whoami_page_no_user(self):
"""Whoami page not accessible without a logged-in user."""
@ -1059,6 +1058,7 @@ class TestDomainPermissions(TestWithDomainPermissions):
"domain-users",
"domain-users-add",
"domain-nameservers",
"domain-org-name-address",
"domain-authorizing-official",
"domain-your-contact-information",
"domain-security-email",
@ -1079,6 +1079,7 @@ class TestDomainPermissions(TestWithDomainPermissions):
"domain-users",
"domain-users-add",
"domain-nameservers",
"domain-org-name-address",
"domain-authorizing-official",
"domain-your-contact-information",
"domain-security-email",
@ -1316,6 +1317,42 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
)
self.assertContains(page, "Testy")
def test_domain_org_name_address(self):
"""Can load domain's org name and mailing address page."""
page = self.client.get(
reverse("domain-org-name-address", kwargs={"pk": self.domain.id})
)
# once on the sidebar, once in the page title, once as H1
self.assertContains(page, "Organization name and mailing address", count=3)
def test_domain_org_name_address_content(self):
"""Org name and address information appears on the page."""
self.domain_information.organization_name = "Town of Igorville"
self.domain_information.save()
page = self.app.get(
reverse("domain-org-name-address", kwargs={"pk": self.domain.id})
)
self.assertContains(page, "Town of Igorville")
def test_domain_org_name_address_form(self):
"""Submitting changes works on the org name address page."""
self.domain_information.organization_name = "Town of Igorville"
self.domain_information.save()
org_name_page = self.app.get(
reverse("domain-org-name-address", kwargs={"pk": self.domain.id})
)
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
org_name_page.form["organization_name"] = "Not igorville"
org_name_page.form["city"] = "Faketown"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
success_result_page = org_name_page.form.submit()
self.assertEqual(success_result_page.status_code, 200)
self.assertContains(success_result_page, "Not igorville")
self.assertContains(success_result_page, "Faketown")
def test_domain_your_contact_information(self):
"""Can load domain's your contact information page."""
page = self.client.get(

View file

@ -2,6 +2,7 @@ from .application import *
from .domain import (
DomainView,
DomainAuthorizingOfficialView,
DomainOrgNameAddressView,
DomainNameserversView,
DomainYourContactInformationView,
DomainSecurityEmailView,

View file

@ -1,6 +1,5 @@
import logging
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect, render
from django.urls import resolve, reverse
@ -44,7 +43,7 @@ class Step(StrEnum):
REVIEW = "review"
class ApplicationWizard(LoginRequiredMixin, TemplateView):
class ApplicationWizard(TemplateView):
"""
A common set of methods and configuration.
@ -502,6 +501,6 @@ class ApplicationWithdrawn(DomainApplicationPermissionView):
to withdraw and send back to homepage.
"""
application = DomainApplication.objects.get(id=self.kwargs["pk"])
application.status = "withdrawn"
application.withdraw()
application.save()
return HttpResponseRedirect(reverse("home"))

View file

@ -22,10 +22,11 @@ from registrar.models import (
)
from ..forms import (
DomainAddUserForm,
NameserverFormset,
DomainSecurityEmailForm,
ContactForm,
DomainOrgNameAddressForm,
DomainAddUserForm,
DomainSecurityEmailForm,
NameserverFormset,
)
from ..utility.email import send_templated_email, EmailSendingError
from .utility import DomainPermissionView, DomainInvitationPermissionDeleteView
@ -41,6 +42,47 @@ class DomainView(DomainPermissionView):
template_name = "domain_detail.html"
class DomainOrgNameAddressView(DomainPermissionView, FormMixin):
"""Organization name and mailing address view"""
model = Domain
template_name = "domain_org_name_address.html"
context_object_name = "domain"
form_class = DomainOrgNameAddressForm
def get_form_kwargs(self, *args, **kwargs):
"""Add domain_info.organization_name instance to make a bound form."""
form_kwargs = super().get_form_kwargs(*args, **kwargs)
form_kwargs["instance"] = self.get_object().domain_info
return form_kwargs
def get_success_url(self):
"""Redirect to the overview page for the domain."""
return reverse("domain-org-name-address", kwargs={"pk": self.object.pk})
def post(self, request, *args, **kwargs):
"""Form submission posts to this view.
This post method harmonizes using DetailView and FormMixin together.
"""
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
"""The form is valid, save the organization name and mailing address."""
form.save()
messages.success(
self.request, "The organization name and mailing address has been updated."
)
# superclass has the redirect
return super().form_valid(form)
class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin):
"""Domain authorizing official editing view."""

View file

@ -1,6 +1,11 @@
from django.http import HttpResponse
from login_required import login_not_required
# the health check endpoint needs to be globally available so that the
# PaaS orchestrator can make sure the app has come up properly
@login_not_required
def health(request):
return HttpResponse(
'<html lang="en"><head><title>OK - Get.gov</title></head><body>OK</body>'

View file

@ -17,6 +17,7 @@ django-auditlog==2.3.0
django-cache-url==3.4.4
django-csp==3.7
django-fsm==2.8.1
django-login-required-middleware==0.9.0
django-phonenumber-field[phonenumberslite]==7.1.0
django-widget-tweaks==1.4.12
environs[django]==9.5.0
@ -47,6 +48,6 @@ s3transfer==0.6.1 ; python_version >= '3.7'
setuptools==67.8.0 ; python_version >= '3.7'
six==1.16.0 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
sqlparse==0.4.4 ; python_version >= '3.5'
typing-extensions==4.6.2
typing-extensions==4.6.3
urllib3==1.26.16 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
whitenoise==6.4.0

View file

@ -27,6 +27,8 @@
10027 OUTOFSCOPE http://app:8080/public/debug_toolbar/js/toolbar.js
# USWDS.min.js contains suspicious words "query", "select", "from" in ordinary usage
10027 OUTOFSCOPE http://app:8080/public/js/uswds.min.js
# UNCLEAR WHY THIS ONE IS FAILING. Giving 404 error.
10027 OUTOFSCOPE http://app:8080/public/js/uswds-init.min.js
# get-gov.js contains suspicious word "from" as in `Array.from()`
10027 OUTOFSCOPE http://app:8080/public/js/get-gov.js
10028 FAIL (Open Redirect - Passive/beta)
@ -53,6 +55,7 @@
10038 OUTOFSCOPE http://app:8080/users/add
10038 OUTOFSCOPE http://app:8080/nameservers
10038 OUTOFSCOPE http://app:8080/your-contact-information
10038 OUTOFSCOPE http://app:8080/authorizing-official
10038 OUTOFSCOPE http://app:8080/security-email
10038 OUTOFSCOPE http://app:8080/delete
10038 OUTOFSCOPE http://app:8080/withdraw
@ -61,6 +64,7 @@
10038 OUTOFSCOPE http://app:8080/todo
# OIDC isn't configured in the test environment and DEBUG=True so this gives a 500 without CSP headers
10038 OUTOFSCOPE http://app:8080/openid/login/
10038 OUTOFSCOPE http://app:8080/openid/logout/
10039 FAIL (X-Backend-Server Header Information Leak - Passive/beta)
10040 FAIL (Secure Pages Include Mixed Content - Passive/release)
10041 FAIL (HTTP to HTTPS Insecure Transition in Form Post - Passive/beta)
@ -83,6 +87,10 @@
10062 FAIL (PII Disclosure - Passive/beta)
10095 FAIL (Backup File Disclosure - Active/beta)
10096 FAIL (Timestamp Disclosure - Passive/release)
# Our sortable table of domains uses timestamps as sort keys so this appears as
# a false-positive to the OWASP scanner
10096 OUTOFSCOPE http://app:8080
10096 OUTOFSCOPE http://app:8080/
10097 FAIL (Hash Disclosure - Passive/beta)
10098 FAIL (Cross-Domain Misconfiguration - Passive/release)
10104 FAIL (User Agent Fuzzer - Active/beta)
@ -147,6 +155,7 @@
# OIDC isn't configured in the test environment and DEBUG=True so these error pages
# trigger this rule in a way that they won't in production
90022 OUTOFSCOPE http://app:8080/openid/login/
90022 OUTOFSCOPE http://app:8080/openid/logout/
90023 FAIL (XML External Entity Attack - Active/beta)
90024 FAIL (Generic Padding Oracle - Active/beta)
90025 FAIL (Expression Language Injection - Active/beta)