mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-06-01 10:13:55 +02:00
Merge remote-tracking branch 'origin/main' into rjm/472-email-domain-in-review
This commit is contained in:
commit
76b0a72ad4
41 changed files with 684 additions and 93 deletions
15
.github/ISSUE_TEMPLATE/bug.yml
vendored
15
.github/ISSUE_TEMPLATE/bug.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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:
|
2
.github/workflows/deploy-stable.yaml
vendored
2
.github/workflows/deploy-stable.yaml
vendored
|
@ -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:
|
||||
|
|
4
.github/workflows/security-check.yaml
vendored
4
.github/workflows/security-check.yaml
vendored
|
@ -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
|
||||
|
|
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
105
docs/developer/registry-access.md
Normal file
105
docs/developer/registry-access.md
Normal 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.
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
35
src/Pipfile.lock
generated
|
@ -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": [
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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----------------------------------------------------------###
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,5 +3,6 @@ from .domain import (
|
|||
DomainAddUserForm,
|
||||
NameserverFormset,
|
||||
DomainSecurityEmailForm,
|
||||
DomainOrgNameAddressForm,
|
||||
ContactForm,
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -578,6 +578,10 @@ class DomainApplication(TimeStampedModel):
|
|||
# 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 ###
|
||||
#
|
||||
# These methods control what questions need to be answered by applicants
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n static %}
|
||||
{% load url_helpers %}
|
||||
|
||||
{% block title %}{% translate "Forbidden | " %}{% endblock %}
|
||||
|
||||
|
@ -22,10 +23,10 @@
|
|||
{% 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 %}
|
||||
|
|
|
@ -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 can’t find what you’re 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 can’t find what you’re looking for, <a href="{% public_site_url 'contact' %}"> contact us </a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -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 %}
|
||||
|
||||
|
|
|
@ -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>We’ll 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 %}
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" %}
|
||||
|
||||
|
|
|
@ -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 %}
|
||||
|
|
47
src/registrar/templates/domain_org_name_address.html
Normal file
47
src/registrar/templates/domain_org_name_address.html
Normal 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 #}
|
|
@ -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 individual’s email. We recommend using an alias, like security@domain.gov.</p>
|
||||
|
||||
|
|
|
@ -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 %}
|
||||
>
|
||||
|
|
|
@ -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"
|
||||
|
|
159
src/registrar/tests/test_url_auth.py
Normal file
159
src/registrar/tests/test_url_auth.py
Normal 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)
|
|
@ -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(
|
||||
|
|
|
@ -2,6 +2,7 @@ from .application import *
|
|||
from .domain import (
|
||||
DomainView,
|
||||
DomainAuthorizingOfficialView,
|
||||
DomainOrgNameAddressView,
|
||||
DomainNameserversView,
|
||||
DomainYourContactInformationView,
|
||||
DomainSecurityEmailView,
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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>'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue