mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-18 07:24:10 +02:00
Merge branch 'main' into ik/match-styles
This commit is contained in:
commit
448535fc55
31 changed files with 1197 additions and 120 deletions
BIN
docs/architecture/diagrams/get.gov registrar deployment.png
Normal file
BIN
docs/architecture/diagrams/get.gov registrar deployment.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 163 KiB |
65
docs/architecture/diagrams/get.gov registrar deployment.puml
Normal file
65
docs/architecture/diagrams/get.gov registrar deployment.puml
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
@startuml get.gov registrar deployment
|
||||||
|
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Deployment.puml
|
||||||
|
|
||||||
|
LAYOUT_WITH_LEGEND()
|
||||||
|
title get.gov registrar deployment
|
||||||
|
skinparam linetype polyline
|
||||||
|
|
||||||
|
Person(team, "get.gov developer", "Code Writer")
|
||||||
|
|
||||||
|
Deployment_Node(aws, "AWS GovCloud", "Amazon Web Services Region") {
|
||||||
|
Deployment_Node(cloudgov, "cloud.gov", "Cloud Foundry PaaS") {
|
||||||
|
System_Ext(cloudgov_router, "cloud.gov router", "Cloud Foundry service")
|
||||||
|
System_Ext(cloudgov_uaa, "cloud.gov authentication", "Cloud Foundry service")
|
||||||
|
System_Ext(cloudgov_controller, "cloud.gov controller", "Cloud Foundry orchestration")
|
||||||
|
System_Ext(cloudgov_dashboard, "cloud.gov dashboard", "Cloud Foundry web UI")
|
||||||
|
System_Ext(cloudgov_logdrain, "logs.fr.cloud.gov", "ELK")
|
||||||
|
Boundary(atob, "ATO boundary") {
|
||||||
|
Deployment_Node(organization, "get.gov organization") {
|
||||||
|
Deployment_Node(unstable, "unstable space") {
|
||||||
|
System_Boundary(dashboard_unstable, "get.gov registrar") {
|
||||||
|
Container(getgov_app_unstable, "Registrar Application", "Python, Django", "Delivers static HTML/CSS and forms")
|
||||||
|
ContainerDb(dashboard_db_unstable, "Unstable PostgreSQL Database", "AWS RDS", "Stores agency information and reports")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Deployment_Node(staging, "staging space") {
|
||||||
|
System_Boundary(dashboard_staging, "get.gov registrar") {
|
||||||
|
Container(getgov_app_staging, "Registrar Application", "Python, Django", "Delivers static HTML/CSS and forms")
|
||||||
|
ContainerDb(dashboard_db_staging, "Staging PostgreSQL Database", "AWS RDS", "Stores agency information and reports")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
' Logs flow
|
||||||
|
Rel(staging, cloudgov_logdrain, "logs to", "stdout/stderr")
|
||||||
|
Rel(team, cloudgov_logdrain, "reviews logs", "https (443)")
|
||||||
|
|
||||||
|
Rel(team, cloudgov_uaa, "authenticates with", "https (443)")
|
||||||
|
Rel(team, cloudgov_dashboard, "inspects", "https (443)")
|
||||||
|
Rel(cloudgov_dashboard, cloudgov_controller, "inspects and manipulates state", "https (443)")
|
||||||
|
|
||||||
|
' Deployment
|
||||||
|
Boundary(deploymentservices, "Deployment services") {
|
||||||
|
Deployment_Node(github, "CI/CD Pipeline", "open source"){
|
||||||
|
System(github_repo, "cisagov/getgov", "Code repository")
|
||||||
|
System_Ext(github_actions_deploy, "github actions", "deploy")
|
||||||
|
System_Ext(github_actions_test, "github actions", "test, security check")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Rel(github_repo, github_actions_test, "pushes to all branches trigger test suite")
|
||||||
|
Rel(github_repo, github_actions_deploy, "pushes to main trigger deployment")
|
||||||
|
Rel(team, github_repo, "commits code, makes pull-request, approves PRs", "https (443)")
|
||||||
|
Rel(github_actions_deploy, cloudgov_controller, "pushes code, invokes tasks", "https (443)")
|
||||||
|
Rel(github_actions_deploy, cloudgov_router, "runs smoke tests on URLs", "https (443)")
|
||||||
|
Rel(cloudgov_controller, staging, "provisions/operates apps and services", "admin access limited")
|
||||||
|
Rel(cloudgov_controller, unstable, "provisions/operates apps and services")
|
||||||
|
|
||||||
|
Rel(getgov_app_staging, dashboard_db_staging, "reads agency info, reads/writes reports, ", "postgres (5432)")
|
||||||
|
Rel(getgov_app_unstable, dashboard_db_unstable, "reads agency info, reads/writes reports, ", "postgres (5432)")
|
||||||
|
|
||||||
|
Rel(cloudgov_router, getgov_app_staging, "proxies to", "https GET/POST (443)")
|
||||||
|
|
||||||
|
@enduml
|
|
@ -78,6 +78,21 @@ To test behind logged in pages with external tools, like `pa11y-ci` or `OWASP Za
|
||||||
|
|
||||||
to MIDDLEWARE in settings.py. **Remove it when you are finished testing.**
|
to MIDDLEWARE in settings.py. **Remove it when you are finished testing.**
|
||||||
|
|
||||||
|
### Reducing console noise in tests
|
||||||
|
|
||||||
|
Some tests, particularly when using Django's test client, will print errors.
|
||||||
|
|
||||||
|
These errors do not indicate test failure, but can make the output hard to read.
|
||||||
|
|
||||||
|
To silence them, we have a helper function `less_console_noise`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from .common import less_console_noise
|
||||||
|
...
|
||||||
|
with less_console_noise():
|
||||||
|
# <test code goes here>
|
||||||
|
```
|
||||||
|
|
||||||
### Accessibility Scanning
|
### Accessibility Scanning
|
||||||
|
|
||||||
The tool `pa11y-ci` is used to scan pages for compliance with a set of
|
The tool `pa11y-ci` is used to scan pages for compliance with a set of
|
||||||
|
@ -123,3 +138,17 @@ In an effort to keep our domain logic centralized, we are representing the state
|
||||||
objects in the application using the [django-fsm](https://github.com/viewflow/django-fsm)
|
objects in the application using the [django-fsm](https://github.com/viewflow/django-fsm)
|
||||||
library. See the [ADR number 15](../architecture/decisions/0015-use-django-fs.md) for
|
library. See the [ADR number 15](../architecture/decisions/0015-use-django-fs.md) for
|
||||||
more information on the topic.
|
more information on the topic.
|
||||||
|
|
||||||
|
## Login Time Bug
|
||||||
|
|
||||||
|
If you are seeing errors related to openid complaining about issuing a token from the future like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
ERROR [djangooidc.oidc:243] Issued in the future
|
||||||
|
```
|
||||||
|
|
||||||
|
it may help to resync your laptop with time.nist.gov:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo sntp -sS time.nist.gov
|
||||||
|
```
|
||||||
|
|
49
src/api/tests/common.py
Normal file
49
src/api/tests/common.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
|
||||||
|
def get_handlers():
|
||||||
|
"""Obtain pointers to all StreamHandlers."""
|
||||||
|
handlers = {}
|
||||||
|
|
||||||
|
rootlogger = logging.getLogger()
|
||||||
|
for h in rootlogger.handlers:
|
||||||
|
if isinstance(h, logging.StreamHandler):
|
||||||
|
handlers[h.name] = h
|
||||||
|
|
||||||
|
for logger in logging.Logger.manager.loggerDict.values():
|
||||||
|
if not isinstance(logger, logging.PlaceHolder):
|
||||||
|
for h in logger.handlers:
|
||||||
|
if isinstance(h, logging.StreamHandler):
|
||||||
|
handlers[h.name] = h
|
||||||
|
|
||||||
|
return handlers
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def less_console_noise():
|
||||||
|
"""
|
||||||
|
Context manager to use in tests to silence console logging.
|
||||||
|
|
||||||
|
This is helpful on tests which trigger console messages
|
||||||
|
(such as errors) which are normal and expected.
|
||||||
|
|
||||||
|
It can easily be removed to debug a failing test.
|
||||||
|
"""
|
||||||
|
restore = {}
|
||||||
|
handlers = get_handlers()
|
||||||
|
devnull = open(os.devnull, "w")
|
||||||
|
|
||||||
|
# redirect all the streams
|
||||||
|
for handler in handlers.values():
|
||||||
|
prior = handler.setStream(devnull)
|
||||||
|
restore[handler.name] = prior
|
||||||
|
try:
|
||||||
|
# run the test
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
# restore the streams
|
||||||
|
for handler in handlers.values():
|
||||||
|
handler.setStream(restore[handler.name])
|
|
@ -7,6 +7,7 @@ from django.contrib.auth import get_user_model
|
||||||
from django.test import TestCase, RequestFactory
|
from django.test import TestCase, RequestFactory
|
||||||
|
|
||||||
from ..views import available, _domains, in_domains
|
from ..views import available, _domains, in_domains
|
||||||
|
from .common import less_console_noise
|
||||||
|
|
||||||
API_BASE_PATH = "/api/v1/available/"
|
API_BASE_PATH = "/api/v1/available/"
|
||||||
|
|
||||||
|
@ -104,10 +105,12 @@ class AvailableAPITest(TestCase):
|
||||||
|
|
||||||
def test_available_post(self):
|
def test_available_post(self):
|
||||||
"""Cannot post to the /available/ API endpoint."""
|
"""Cannot post to the /available/ API endpoint."""
|
||||||
response = self.client.post(API_BASE_PATH + "nonsense")
|
with less_console_noise():
|
||||||
|
response = self.client.post(API_BASE_PATH + "nonsense")
|
||||||
self.assertEqual(response.status_code, 405)
|
self.assertEqual(response.status_code, 405)
|
||||||
|
|
||||||
def test_available_bad_input(self):
|
def test_available_bad_input(self):
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
response = self.client.get(API_BASE_PATH + "blah!;")
|
with less_console_noise():
|
||||||
|
response = self.client.get(API_BASE_PATH + "blah!;")
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
|
@ -11,7 +11,7 @@ import requests
|
||||||
|
|
||||||
from cachetools.func import ttl_cache
|
from cachetools.func import ttl_cache
|
||||||
|
|
||||||
from registrar.models import Website
|
from registrar.models import Domain
|
||||||
|
|
||||||
DOMAIN_FILE_URL = (
|
DOMAIN_FILE_URL = (
|
||||||
"https://raw.githubusercontent.com/cisagov/dotgov-data/main/current-full.csv"
|
"https://raw.githubusercontent.com/cisagov/dotgov-data/main/current-full.csv"
|
||||||
|
@ -35,7 +35,7 @@ def _domains():
|
||||||
# get the domain before the first comma
|
# get the domain before the first comma
|
||||||
domain = line.split(",", 1)[0]
|
domain = line.split(",", 1)[0]
|
||||||
# sanity-check the string we got from the file here
|
# sanity-check the string we got from the file here
|
||||||
if Website.string_could_be_domain(domain):
|
if Domain.string_could_be_domain(domain):
|
||||||
# lowercase everything when we put it in domains
|
# lowercase everything when we put it in domains
|
||||||
domains.add(domain.lower())
|
domains.add(domain.lower())
|
||||||
return domains
|
return domains
|
||||||
|
@ -68,8 +68,8 @@ def available(request, domain=""):
|
||||||
# validate that the given domain could be a domain name and fail early if
|
# validate that the given domain could be a domain name and fail early if
|
||||||
# not.
|
# not.
|
||||||
if not (
|
if not (
|
||||||
Website.string_could_be_domain(domain)
|
Domain.string_could_be_domain(domain)
|
||||||
or Website.string_could_be_domain(domain + ".gov")
|
or Domain.string_could_be_domain(domain + ".gov")
|
||||||
):
|
):
|
||||||
raise BadRequest("Invalid request.")
|
raise BadRequest("Invalid request.")
|
||||||
# a domain is available if it is NOT in the list of current domains
|
# a domain is available if it is NOT in the list of current domains
|
||||||
|
|
0
src/epp/__init__.py
Normal file
0
src/epp/__init__.py
Normal file
40
src/epp/mock_epp.py
Normal file
40
src/epp/mock_epp.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
"""
|
||||||
|
This file defines a number of mock functions which can be used to simulate
|
||||||
|
communication with the registry until that integration is implemented.
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
def domain_check(_):
|
||||||
|
"""Is domain available for registration?"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def domain_info(domain):
|
||||||
|
"""What does the registry know about this domain?"""
|
||||||
|
return {
|
||||||
|
"name": domain,
|
||||||
|
"roid": "EXAMPLE1-REP",
|
||||||
|
"status": ["ok"],
|
||||||
|
"registrant": "jd1234",
|
||||||
|
"contact": {
|
||||||
|
"admin": "sh8013",
|
||||||
|
"tech": None,
|
||||||
|
},
|
||||||
|
"ns": {
|
||||||
|
f"ns1.{domain}",
|
||||||
|
f"ns2.{domain}",
|
||||||
|
},
|
||||||
|
"host": [
|
||||||
|
f"ns1.{domain}",
|
||||||
|
f"ns2.{domain}",
|
||||||
|
],
|
||||||
|
"sponsor": "ClientX",
|
||||||
|
"creator": "ClientY",
|
||||||
|
# TODO: think about timezones
|
||||||
|
"creation_date": datetime.today(),
|
||||||
|
"updator": "ClientX",
|
||||||
|
"last_update_date": datetime.today(),
|
||||||
|
"expiration_date": datetime.today(),
|
||||||
|
"last_transfer_date": datetime.today(),
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||||
from django.http.response import HttpResponseRedirect
|
from django.http.response import HttpResponseRedirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from .models import User, UserProfile, DomainApplication, Website
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
class AuditedAdmin(admin.ModelAdmin):
|
class AuditedAdmin(admin.ModelAdmin):
|
||||||
|
@ -26,7 +26,7 @@ class UserProfileInline(admin.StackedInline):
|
||||||
|
|
||||||
"""Edit a user's profile on the user page."""
|
"""Edit a user's profile on the user page."""
|
||||||
|
|
||||||
model = UserProfile
|
model = models.UserProfile
|
||||||
|
|
||||||
|
|
||||||
class MyUserAdmin(UserAdmin):
|
class MyUserAdmin(UserAdmin):
|
||||||
|
@ -36,6 +36,24 @@ class MyUserAdmin(UserAdmin):
|
||||||
inlines = [UserProfileInline]
|
inlines = [UserProfileInline]
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(User, MyUserAdmin)
|
class HostIPInline(admin.StackedInline):
|
||||||
admin.site.register(DomainApplication, AuditedAdmin)
|
|
||||||
admin.site.register(Website, AuditedAdmin)
|
"""Edit an ip address on the host page."""
|
||||||
|
|
||||||
|
model = models.HostIP
|
||||||
|
|
||||||
|
|
||||||
|
class MyHostAdmin(AuditedAdmin):
|
||||||
|
|
||||||
|
"""Custom host admin class to use our inlines."""
|
||||||
|
|
||||||
|
inlines = [HostIPInline]
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(models.User, MyUserAdmin)
|
||||||
|
admin.site.register(models.Contact, AuditedAdmin)
|
||||||
|
admin.site.register(models.DomainApplication, AuditedAdmin)
|
||||||
|
admin.site.register(models.Domain, AuditedAdmin)
|
||||||
|
admin.site.register(models.Host, MyHostAdmin)
|
||||||
|
admin.site.register(models.Nameserver, MyHostAdmin)
|
||||||
|
admin.site.register(models.Website, AuditedAdmin)
|
||||||
|
|
BIN
src/registrar/assets/img/CISA_logo.png
Normal file
BIN
src/registrar/assets/img/CISA_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 322 KiB |
5
src/registrar/assets/img/dottedgov-round.svg
Normal file
5
src/registrar/assets/img/dottedgov-round.svg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="-0.14" y="0.382" rx="15" ry="15" width="501.007" height="500.135" style="fill: rgb(0, 82, 136);"/>
|
||||||
|
<text x="-1.813" y="338.582" style="fill: rgb(0, 190, 200); font-family: Helvetica; font-size: 240px; font-weight: bold; white-space: pre;">.gov</text>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 373 B |
|
@ -10,12 +10,14 @@ from django.urls import include, path
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
|
|
||||||
from registrar.views import health, index, profile, whoami
|
from registrar.views import health, index, profile, whoami
|
||||||
from registrar.forms import ApplicationWizard
|
from registrar.forms import ApplicationWizard, WIZARD_CONDITIONS
|
||||||
from api.views import available
|
from api.views import available
|
||||||
|
|
||||||
APPLICATION_URL_NAME = "application_step"
|
APPLICATION_URL_NAME = "application_step"
|
||||||
application_wizard = ApplicationWizard.as_view(
|
application_wizard = ApplicationWizard.as_view(
|
||||||
url_name=APPLICATION_URL_NAME, done_step_name="finished"
|
url_name=APPLICATION_URL_NAME,
|
||||||
|
done_step_name="finished",
|
||||||
|
condition_dict=WIZARD_CONDITIONS,
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from .edit_profile import EditProfileForm
|
from .edit_profile import EditProfileForm
|
||||||
from .application_wizard import ApplicationWizard
|
from .application_wizard import ApplicationWizard, WIZARD_CONDITIONS
|
||||||
|
|
||||||
__all__ = ["EditProfileForm", "ApplicationWizard"]
|
__all__ = ["EditProfileForm", "ApplicationWizard", "WIZARD_CONDITIONS"]
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
"""Forms Wizard for creating a new domain application."""
|
"""Forms Wizard for creating a new domain application."""
|
||||||
|
|
||||||
|
from __future__ import annotations # allows forward references in annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
@ -9,7 +11,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
|
||||||
from formtools.wizard.views import NamedUrlSessionWizardView # type: ignore
|
from formtools.wizard.views import NamedUrlSessionWizardView # type: ignore
|
||||||
|
|
||||||
from registrar.models import DomainApplication, Website
|
from registrar.models import DomainApplication, Domain
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -52,19 +54,6 @@ class OrganizationTypeForm(RegistrarForm):
|
||||||
],
|
],
|
||||||
widget=forms.RadioSelect,
|
widget=forms.RadioSelect,
|
||||||
)
|
)
|
||||||
federal_type = forms.ChoiceField(
|
|
||||||
required=False,
|
|
||||||
choices=DomainApplication.BRANCH_CHOICES,
|
|
||||||
widget=forms.RadioSelect,
|
|
||||||
)
|
|
||||||
is_election_board = forms.ChoiceField(
|
|
||||||
required=False,
|
|
||||||
choices=[
|
|
||||||
("Yes", "Yes"),
|
|
||||||
("No", "No"),
|
|
||||||
],
|
|
||||||
widget=forms.RadioSelect(attrs={"class": "usa-radio__input"}),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationFederalForm(RegistrarForm):
|
class OrganizationFederalForm(RegistrarForm):
|
||||||
|
@ -82,7 +71,8 @@ class OrganizationElectionForm(RegistrarForm):
|
||||||
(True, "Yes"),
|
(True, "Yes"),
|
||||||
(False, "No"),
|
(False, "No"),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
|
required=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -297,6 +287,14 @@ TITLES = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# We can use a dictionary with step names and callables that return booleans
|
||||||
|
# to show or hide particular steps based on the state of the process.
|
||||||
|
WIZARD_CONDITIONS = {
|
||||||
|
"organization_federal": DomainApplication.show_organization_federal,
|
||||||
|
"organization_election": DomainApplication.show_organization_election,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ApplicationWizard(LoginRequiredMixin, NamedUrlSessionWizardView):
|
class ApplicationWizard(LoginRequiredMixin, NamedUrlSessionWizardView):
|
||||||
|
|
||||||
"""Multi-page form ("wizard") for new domain applications.
|
"""Multi-page form ("wizard") for new domain applications.
|
||||||
|
@ -330,13 +328,17 @@ class ApplicationWizard(LoginRequiredMixin, NamedUrlSessionWizardView):
|
||||||
organization_type_data = form_dict["organization_type"].cleaned_data
|
organization_type_data = form_dict["organization_type"].cleaned_data
|
||||||
application.organization_type = organization_type_data["organization_type"]
|
application.organization_type = organization_type_data["organization_type"]
|
||||||
|
|
||||||
# federal branch information
|
# federal branch information may not exist
|
||||||
federal_branch_data = form_dict["organization_federal"].cleaned_data
|
federal_branch_data = form_dict.get("organization_federal")
|
||||||
application.federal_branch = federal_branch_data["federal_type"]
|
if federal_branch_data is not None:
|
||||||
|
federal_branch_data = federal_branch_data.cleaned_data
|
||||||
|
application.federal_branch = federal_branch_data["federal_type"]
|
||||||
|
|
||||||
# election board information
|
# election board information may not exist.
|
||||||
election_board_data = form_dict["organization_election"].cleaned_data
|
election_board_data = form_dict.get("organization_election")
|
||||||
application.is_election_office = election_board_data["is_election_board"]
|
if election_board_data is not None:
|
||||||
|
election_board_data = election_board_data.cleaned_data
|
||||||
|
application.is_election_office = election_board_data["is_election_board"]
|
||||||
|
|
||||||
# contact information
|
# contact information
|
||||||
contact_data = form_dict["organization_contact"].cleaned_data
|
contact_data = form_dict["organization_contact"].cleaned_data
|
||||||
|
@ -346,8 +348,8 @@ class ApplicationWizard(LoginRequiredMixin, NamedUrlSessionWizardView):
|
||||||
|
|
||||||
# This isn't really the requested_domain field
|
# This isn't really the requested_domain field
|
||||||
# but we need something in this field to make the form submittable
|
# but we need something in this field to make the form submittable
|
||||||
requested_site, _ = Website.objects.get_or_create(
|
requested_site, _ = Domain.objects.get_or_create(
|
||||||
website=contact_data["organization_name"] + ".gov"
|
name=contact_data["organization_name"] + ".gov"
|
||||||
)
|
)
|
||||||
application.requested_domain = requested_site
|
application.requested_domain = requested_site
|
||||||
return application
|
return application
|
||||||
|
|
|
@ -0,0 +1,165 @@
|
||||||
|
# Generated by Django 4.1.3 on 2022-11-28 19:07
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django_fsm # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Domain",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"name",
|
||||||
|
models.CharField(
|
||||||
|
default=None,
|
||||||
|
help_text="Fully qualified domain name",
|
||||||
|
max_length=253,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"is_active",
|
||||||
|
django_fsm.FSMField(
|
||||||
|
choices=[(True, "Yes"), (False, "No")],
|
||||||
|
default=False,
|
||||||
|
help_text="Domain is live in the registry",
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("owners", models.ManyToManyField(to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Host",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"name",
|
||||||
|
models.CharField(
|
||||||
|
default=None,
|
||||||
|
help_text="Fully qualified domain name",
|
||||||
|
max_length=253,
|
||||||
|
unique=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"domain",
|
||||||
|
models.ForeignKey(
|
||||||
|
help_text="Domain to which this host belongs",
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="host",
|
||||||
|
to="registrar.domain",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Nameserver",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"host_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="registrar.host",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
bases=("registrar.host",),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="HostIP",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"address",
|
||||||
|
models.CharField(
|
||||||
|
default=None,
|
||||||
|
help_text="IP address",
|
||||||
|
max_length=46,
|
||||||
|
validators=[django.core.validators.validate_ipv46_address],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"host",
|
||||||
|
models.ForeignKey(
|
||||||
|
help_text="Host to which this IP address belongs",
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="ip",
|
||||||
|
to="registrar.host",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainapplication",
|
||||||
|
name="requested_domain",
|
||||||
|
field=models.OneToOneField(
|
||||||
|
blank=True,
|
||||||
|
help_text="The requested domain",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="domain_application",
|
||||||
|
to="registrar.domain",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="domain",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
condition=models.Q(("is_active", True)),
|
||||||
|
fields=("name",),
|
||||||
|
name="unique_domain_name_in_registry",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -2,6 +2,10 @@ from auditlog.registry import auditlog # type: ignore
|
||||||
|
|
||||||
from .contact import Contact
|
from .contact import Contact
|
||||||
from .domain_application import DomainApplication
|
from .domain_application import DomainApplication
|
||||||
|
from .domain import Domain
|
||||||
|
from .host_ip import HostIP
|
||||||
|
from .host import Host
|
||||||
|
from .nameserver import Nameserver
|
||||||
from .user_profile import UserProfile
|
from .user_profile import UserProfile
|
||||||
from .user import User
|
from .user import User
|
||||||
from .website import Website
|
from .website import Website
|
||||||
|
@ -9,6 +13,10 @@ from .website import Website
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Contact",
|
"Contact",
|
||||||
"DomainApplication",
|
"DomainApplication",
|
||||||
|
"Domain",
|
||||||
|
"HostIP",
|
||||||
|
"Host",
|
||||||
|
"Nameserver",
|
||||||
"UserProfile",
|
"UserProfile",
|
||||||
"User",
|
"User",
|
||||||
"Website",
|
"Website",
|
||||||
|
@ -16,6 +24,10 @@ __all__ = [
|
||||||
|
|
||||||
auditlog.register(Contact)
|
auditlog.register(Contact)
|
||||||
auditlog.register(DomainApplication)
|
auditlog.register(DomainApplication)
|
||||||
|
auditlog.register(Domain)
|
||||||
|
auditlog.register(HostIP)
|
||||||
|
auditlog.register(Host)
|
||||||
|
auditlog.register(Nameserver)
|
||||||
auditlog.register(UserProfile)
|
auditlog.register(UserProfile)
|
||||||
auditlog.register(User)
|
auditlog.register(User)
|
||||||
auditlog.register(Website)
|
auditlog.register(Website)
|
||||||
|
|
237
src/registrar/models/domain.py
Normal file
237
src/registrar/models/domain.py
Normal file
|
@ -0,0 +1,237 @@
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django_fsm import FSMField, transition # type: ignore
|
||||||
|
|
||||||
|
from epp.mock_epp import domain_info, domain_check
|
||||||
|
|
||||||
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
|
from .domain_application import DomainApplication
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Domain(TimeStampedModel):
|
||||||
|
"""
|
||||||
|
Manage the lifecycle of domain names.
|
||||||
|
|
||||||
|
The registry is the source of truth for this data and this model exists:
|
||||||
|
1. To tie ownership information in the registrar to
|
||||||
|
DNS entries in the registry; and
|
||||||
|
2. To allow a new registrant to draft DNS entries before their
|
||||||
|
application is approved
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
# draft domains may share the same name, but
|
||||||
|
# once approved, they must be globally unique
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["name"],
|
||||||
|
condition=models.Q(is_active=True),
|
||||||
|
name="unique_domain_name_in_registry",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
class Status(models.TextChoices):
|
||||||
|
"""
|
||||||
|
The status codes we can receive from the registry.
|
||||||
|
|
||||||
|
These are detailed in RFC 5731 in section 2.3.
|
||||||
|
https://www.rfc-editor.org/std/std69.txt
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Requests to delete the object MUST be rejected.
|
||||||
|
CLIENT_DELETE_PROHIBITED = "clientDeleteProhibited"
|
||||||
|
SERVER_DELETE_PROHIBITED = "serverDeleteProhibited"
|
||||||
|
|
||||||
|
# DNS delegation information MUST NOT be published for the object.
|
||||||
|
CLIENT_HOLD = "clientHold"
|
||||||
|
SERVER_HOLD = "serverHold"
|
||||||
|
|
||||||
|
# Requests to renew the object MUST be rejected.
|
||||||
|
CLIENT_RENEW_PROHIBITED = "clientRenewProhibited"
|
||||||
|
SERVER_RENEW_PROHIBITED = "serverRenewProhibited"
|
||||||
|
|
||||||
|
# Requests to transfer the object MUST be rejected.
|
||||||
|
CLIENT_TRANSFER_PROHIBITED = "clientTransferProhibited"
|
||||||
|
SERVER_TRANSFER_PROHIBITED = "serverTransferProhibited"
|
||||||
|
|
||||||
|
# Requests to update the object (other than to remove this status)
|
||||||
|
# MUST be rejected.
|
||||||
|
CLIENT_UPDATE_PROHIBITED = "clientUpdateProhibited"
|
||||||
|
SERVER_UPDATE_PROHIBITED = "serverUpdateProhibited"
|
||||||
|
|
||||||
|
# Delegation information has not been associated with the object.
|
||||||
|
# This is the default status when a domain object is first created
|
||||||
|
# and there are no associated host objects for the DNS delegation.
|
||||||
|
# This status can also be set by the server when all host-object
|
||||||
|
# associations are removed.
|
||||||
|
INACTIVE = "inactive"
|
||||||
|
|
||||||
|
# This is the normal status value for an object that has no pending
|
||||||
|
# operations or prohibitions. This value is set and removed by the
|
||||||
|
# server as other status values are added or removed.
|
||||||
|
OK = "ok"
|
||||||
|
|
||||||
|
# A transform command has been processed for the object, but the
|
||||||
|
# action has not been completed by the server. Server operators can
|
||||||
|
# delay action completion for a variety of reasons, such as to allow
|
||||||
|
# for human review or third-party action. A transform command that
|
||||||
|
# is processed, but whose requested action is pending, is noted with
|
||||||
|
# response code 1001.
|
||||||
|
PENDING_CREATE = "pendingCreate"
|
||||||
|
PENDING_DELETE = "pendingDelete"
|
||||||
|
PENDING_RENEW = "pendingRenew"
|
||||||
|
PENDING_TRANSFER = "pendingTransfer"
|
||||||
|
PENDING_UPDATE = "pendingUpdate"
|
||||||
|
|
||||||
|
# a domain name is alphanumeric or hyphen, up to 63 characters, doesn't
|
||||||
|
# begin or end with a hyphen, followed by a TLD of 2-6 alphabetic characters
|
||||||
|
DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.[A-Za-z]{2,6}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def string_could_be_domain(cls, domain: str) -> bool:
|
||||||
|
"""Return True if the string could be a domain name, otherwise False."""
|
||||||
|
if cls.DOMAIN_REGEX.match(domain):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def available(cls, domain: str) -> bool:
|
||||||
|
"""Check if a domain is available.
|
||||||
|
|
||||||
|
Not implemented. Returns a dummy value for testing."""
|
||||||
|
return domain_check(domain)
|
||||||
|
|
||||||
|
def transfer(self):
|
||||||
|
"""Going somewhere. Not implemented."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def renew(self):
|
||||||
|
"""Time to renew. Not implemented."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_property(self, property):
|
||||||
|
"""Get some info about a domain."""
|
||||||
|
if not self.is_active:
|
||||||
|
return None
|
||||||
|
if not hasattr(self, "info"):
|
||||||
|
try:
|
||||||
|
# get info from registry
|
||||||
|
self.info = domain_info(self.name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
# TODO: back off error handling
|
||||||
|
return None
|
||||||
|
if hasattr(self, "info"):
|
||||||
|
if property in self.info:
|
||||||
|
return self.info[property]
|
||||||
|
else:
|
||||||
|
raise KeyError(
|
||||||
|
"Requested key %s was not found in registry data." % str(property)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# TODO: return an error if registry cannot be contacted
|
||||||
|
return None
|
||||||
|
|
||||||
|
def could_be_domain(self) -> bool:
|
||||||
|
"""Could this instance be a domain?"""
|
||||||
|
# short-circuit if self.website is null/None
|
||||||
|
if not self.name:
|
||||||
|
return False
|
||||||
|
return self.string_could_be_domain(str(self.name))
|
||||||
|
|
||||||
|
@transition(field="is_active", source="*", target=True)
|
||||||
|
def activate(self):
|
||||||
|
"""This domain should be made live."""
|
||||||
|
if hasattr(self, "domain_application"):
|
||||||
|
if self.domain_application.status != DomainApplication.APPROVED:
|
||||||
|
raise ValueError("Cannot activate. Application must be approved.")
|
||||||
|
if Domain.objects.filter(name=self.name, is_active=True).exists():
|
||||||
|
raise ValueError("Cannot activate. Domain name is already in use.")
|
||||||
|
# TODO: depending on the details of our registry integration
|
||||||
|
# we will either contact the registry and deploy the domain
|
||||||
|
# in this function OR we will verify that it has already been
|
||||||
|
# activated and reject this state transition if it has not
|
||||||
|
pass
|
||||||
|
|
||||||
|
@transition(field="is_active", source="*", target=False)
|
||||||
|
def deactivate(self):
|
||||||
|
"""This domain should not be live."""
|
||||||
|
# there are security concerns to having this function exist
|
||||||
|
# within the codebase; discuss these with the project lead
|
||||||
|
# if there is a feature request to implement this
|
||||||
|
raise Exception("Cannot revoke, contact registry.")
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def roid(self):
|
||||||
|
return self._get_property("roid")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self):
|
||||||
|
return self._get_property("status")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def registrant(self):
|
||||||
|
return self._get_property("registrant")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sponsor(self):
|
||||||
|
return self._get_property("sponsor")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def creator(self):
|
||||||
|
return self._get_property("creator")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def creation_date(self):
|
||||||
|
return self._get_property("creation_date")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def updator(self):
|
||||||
|
return self._get_property("updator")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_update_date(self):
|
||||||
|
return self._get_property("last_update_date")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expiration_date(self):
|
||||||
|
return self._get_property("expiration_date")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_transfer_date(self):
|
||||||
|
return self._get_property("last_transfer_date")
|
||||||
|
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=253,
|
||||||
|
blank=False,
|
||||||
|
default=None, # prevent saving without a value
|
||||||
|
help_text="Fully qualified domain name",
|
||||||
|
)
|
||||||
|
|
||||||
|
# we use `is_active` rather than `domain_application.status`
|
||||||
|
# because domains may exist without associated applications
|
||||||
|
is_active = FSMField(
|
||||||
|
choices=[
|
||||||
|
(True, "Yes"),
|
||||||
|
(False, "No"),
|
||||||
|
],
|
||||||
|
default=False,
|
||||||
|
# TODO: how to edit models in Django admin if protected = True
|
||||||
|
protected=False,
|
||||||
|
help_text="Domain is live in the registry",
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: determine the relationship between this field
|
||||||
|
# and the domain application's `creator` and `submitter`
|
||||||
|
owners = models.ManyToManyField(
|
||||||
|
User,
|
||||||
|
help_text="",
|
||||||
|
)
|
|
@ -1,3 +1,5 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django_fsm import FSMField, transition # type: ignore
|
from django_fsm import FSMField, transition # type: ignore
|
||||||
|
|
||||||
|
@ -6,6 +8,11 @@ from .contact import Contact
|
||||||
from .user import User
|
from .user import User
|
||||||
from .website import Website
|
from .website import Website
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Union
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..forms.application_wizard import ApplicationWizard
|
||||||
|
|
||||||
|
|
||||||
class DomainApplication(TimeStampedModel):
|
class DomainApplication(TimeStampedModel):
|
||||||
|
|
||||||
|
@ -147,12 +154,12 @@ class DomainApplication(TimeStampedModel):
|
||||||
related_name="current+",
|
related_name="current+",
|
||||||
)
|
)
|
||||||
|
|
||||||
requested_domain = models.ForeignKey(
|
requested_domain = models.OneToOneField(
|
||||||
Website,
|
"Domain",
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="The requested domain",
|
help_text="The requested domain",
|
||||||
related_name="requested+",
|
related_name="domain_application",
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
)
|
)
|
||||||
alternative_domains = models.ManyToManyField(
|
alternative_domains = models.ManyToManyField(
|
||||||
|
@ -204,8 +211,8 @@ class DomainApplication(TimeStampedModel):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
try:
|
try:
|
||||||
if self.requested_domain and self.requested_domain.website:
|
if self.requested_domain and self.requested_domain.name:
|
||||||
return self.requested_domain.website
|
return self.requested_domain.name
|
||||||
else:
|
else:
|
||||||
return f"{self.status} application created by {self.creator}"
|
return f"{self.status} application created by {self.creator}"
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -225,3 +232,36 @@ class DomainApplication(TimeStampedModel):
|
||||||
# if no exception was raised, then we don't need to do anything
|
# if no exception was raised, then we don't need to do anything
|
||||||
# inside this method, keep the `pass` here to remind us of that
|
# inside this method, keep the `pass` here to remind us of that
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# ## Form policies ###
|
||||||
|
#
|
||||||
|
# These methods control what questions need to be answered by applicants
|
||||||
|
# during the application flow. They are policies about the application so
|
||||||
|
# they appear here.
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_organization_type(wizard: ApplicationWizard) -> Union[str, None]:
|
||||||
|
"""Extract the answer to the organization type question from the wizard."""
|
||||||
|
# using the step data from the storage is a workaround for this
|
||||||
|
# bug in django-formtools version 2.4
|
||||||
|
# https://github.com/jazzband/django-formtools/issues/220
|
||||||
|
type_data = wizard.storage.get_step_data("organization_type")
|
||||||
|
if type_data:
|
||||||
|
return type_data.get("organization_type-organization_type")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def show_organization_federal(wizard: ApplicationWizard) -> bool:
|
||||||
|
"""Show this step if the answer to the first question was "federal"."""
|
||||||
|
return DomainApplication._get_organization_type(wizard) == "Federal"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def show_organization_election(wizard: ApplicationWizard) -> bool:
|
||||||
|
"""Show this step if the answer to the first question implies it.
|
||||||
|
|
||||||
|
This shows for answers that aren't "Federal" or "Interstate".
|
||||||
|
"""
|
||||||
|
type_answer = DomainApplication._get_organization_type(wizard)
|
||||||
|
if type_answer and type_answer not in ("Federal", "Interstate"):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
33
src/registrar/models/host.py
Normal file
33
src/registrar/models/host.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
|
from .domain import Domain
|
||||||
|
|
||||||
|
|
||||||
|
class Host(TimeStampedModel):
|
||||||
|
"""
|
||||||
|
Hosts are internet-connected computers.
|
||||||
|
|
||||||
|
They may handle email, serve websites, or perform other tasks.
|
||||||
|
|
||||||
|
The registry is the source of truth for this data.
|
||||||
|
|
||||||
|
This model exists ONLY to allow a new registrant to draft DNS entries
|
||||||
|
before their application is approved.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=253,
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
default=None, # prevent saving without a value
|
||||||
|
unique=True,
|
||||||
|
help_text="Fully qualified domain name",
|
||||||
|
)
|
||||||
|
|
||||||
|
domain = models.ForeignKey(
|
||||||
|
Domain,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name="host", # access this Host via the Domain as `domain.host`
|
||||||
|
help_text="Domain to which this host belongs",
|
||||||
|
)
|
32
src/registrar/models/host_ip.py
Normal file
32
src/registrar/models/host_ip.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.core.validators import validate_ipv46_address
|
||||||
|
|
||||||
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
|
from .host import Host
|
||||||
|
|
||||||
|
|
||||||
|
class HostIP(TimeStampedModel):
|
||||||
|
"""
|
||||||
|
Hosts may have one or more IP addresses.
|
||||||
|
|
||||||
|
The registry is the source of truth for this data.
|
||||||
|
|
||||||
|
This model exists ONLY to allow a new registrant to draft DNS entries
|
||||||
|
before their application is approved.
|
||||||
|
"""
|
||||||
|
|
||||||
|
address = models.CharField(
|
||||||
|
max_length=46,
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
default=None, # prevent saving without a value
|
||||||
|
validators=[validate_ipv46_address],
|
||||||
|
help_text="IP address",
|
||||||
|
)
|
||||||
|
|
||||||
|
host = models.ForeignKey(
|
||||||
|
Host,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name="ip", # access this HostIP via the Host as `host.ip`
|
||||||
|
help_text="Host to which this IP address belongs",
|
||||||
|
)
|
16
src/registrar/models/nameserver.py
Normal file
16
src/registrar/models/nameserver.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from .host import Host
|
||||||
|
|
||||||
|
|
||||||
|
class Nameserver(Host):
|
||||||
|
"""
|
||||||
|
A nameserver is a host which has been delegated to respond to DNS queries.
|
||||||
|
|
||||||
|
The registry is the source of truth for this data.
|
||||||
|
|
||||||
|
This model exists ONLY to allow a new registrant to draft DNS entries
|
||||||
|
before their application is approved.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# there is nothing here because all of the fields are
|
||||||
|
# defined over there on the Host class
|
||||||
|
pass
|
|
@ -1,5 +1,3 @@
|
||||||
import re
|
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,26 +14,5 @@ class Website(models.Model):
|
||||||
help_text="",
|
help_text="",
|
||||||
)
|
)
|
||||||
|
|
||||||
# a domain name is alphanumeric or hyphen, up to 63 characters, doesn't
|
|
||||||
# begin or end with a hyphen, followed by a TLD of 2-6 alphabetic characters
|
|
||||||
DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.[A-Za-z]{2,6}")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def string_could_be_domain(cls, domain: str) -> bool:
|
|
||||||
"""Return True if the string could be a domain name, otherwise False.
|
|
||||||
|
|
||||||
TODO: when we have a Domain class, this could be a classmethod there.
|
|
||||||
"""
|
|
||||||
if cls.DOMAIN_REGEX.match(domain):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def could_be_domain(self) -> bool:
|
|
||||||
"""Could this instance be a domain?"""
|
|
||||||
# short-circuit if self.website is null/None
|
|
||||||
if not self.website:
|
|
||||||
return False
|
|
||||||
return self.string_could_be_domain(str(self.website))
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return str(self.website)
|
return str(self.website)
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main id="main-content" class="grid-container">
|
<main id="main-content" class="grid-container">
|
||||||
|
|
||||||
<h2>{% translate "Page not found" %}</h2>
|
<h1>{% translate "Page not found" %}</h1>
|
||||||
|
|
||||||
<p>{% translate "The requested page could not be found." %}</p>
|
<p>{% translate "The requested page could not be found." %}</p>
|
||||||
|
|
||||||
|
|
|
@ -180,15 +180,7 @@
|
||||||
{% block content_bottom %}{% endblock %}
|
{% block content_bottom %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer id="footer" role="contentinfo">
|
{% include "includes/footer.html" %}
|
||||||
{% block footer_nav %}
|
|
||||||
{% endblock %}
|
|
||||||
{% block footer %}
|
|
||||||
<div>
|
|
||||||
<p class="copyright">© {% now "Y" %} CISA .gov Registrar</p>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
</footer>
|
|
||||||
</div> <!-- /#wrapper -->
|
</div> <!-- /#wrapper -->
|
||||||
|
|
||||||
{% block init_js %}{% endblock %}{# useful for vars and other initializations #}
|
{% block init_js %}{% endblock %}{# useful for vars and other initializations #}
|
||||||
|
|
|
@ -6,21 +6,51 @@
|
||||||
<section class="usa-hero">
|
<section class="usa-hero">
|
||||||
<div class="usa-grid">
|
<div class="usa-grid">
|
||||||
<div class="usa-hero-callout usa-section-dark">
|
<div class="usa-hero-callout usa-section-dark">
|
||||||
<h2>
|
<h1>
|
||||||
<span class="usa-hero-callout-alt">Welcome to the .gov registrar</span>
|
<span class="usa-hero-callout-alt">Welcome to the .gov registrar</span>
|
||||||
</h2>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main id="main-content" class="grid-container">
|
<main id="main-content" class="grid-container">
|
||||||
<p>This is the .gov registrar.</p>
|
<p>This is the .gov registrar.</p>
|
||||||
|
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<p><a href="/openid/logout/">Click here to log out.</a></p>
|
|
||||||
{% else %}
|
{% if domain_applications %}
|
||||||
<p><a href="/openid/login/">Click here to log in.</a></p>
|
<h2>Your domain applications</h2>
|
||||||
{% endif %}
|
<table class="usa-table usa-table--borderless">
|
||||||
|
<caption class="sr-only">Your domain applications</caption>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for application in domain_applications %}
|
||||||
|
<tr>
|
||||||
|
<th>{{ application.requested_domain.name }}</th>
|
||||||
|
<td>{{ application.status }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p><a href="{% url 'application' %}" class="usa-button">Apply</a></p>
|
||||||
|
|
||||||
|
<p><a href="{% url 'edit-profile' %}">Edit profile</a></p>
|
||||||
|
|
||||||
|
{% if user.is_staff %}
|
||||||
|
<p><a href="{% url 'admin:index' %}">CISA admin panel</a></p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p><a href="{% url 'logout' %}">Click here to log out.</a></p>
|
||||||
|
{% else %}
|
||||||
|
<p><a href="{% url 'login' %}">Click here to log in.</a></p>
|
||||||
|
{% endif %}
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
124
src/registrar/templates/includes/footer.html
Normal file
124
src/registrar/templates/includes/footer.html
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<footer class="usa-footer">
|
||||||
|
<div class="usa-footer__secondary-section">
|
||||||
|
<div class="grid-container">
|
||||||
|
<div class="grid-row grid-gap">
|
||||||
|
<div
|
||||||
|
class="
|
||||||
|
usa-footer__logo
|
||||||
|
grid-row
|
||||||
|
mobile-lg:grid-col-6 mobile-lg:grid-gap-2
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="mobile-lg:grid-col-auto">
|
||||||
|
<img
|
||||||
|
class="usa-footer__logo-img"
|
||||||
|
src="{% static 'img/dottedgov-round.svg' %}"
|
||||||
|
alt="dot gov registrar logo"
|
||||||
|
width="100px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="usa-footer__contact-links mobile-lg:grid-col-6">
|
||||||
|
<p class="usa-footer__contact-heading">
|
||||||
|
Contact us
|
||||||
|
</p>
|
||||||
|
<address class="usa-footer__address">
|
||||||
|
<div class="usa-footer__contact-info grid-row grid-gap">
|
||||||
|
<div class="grid-col-auto">
|
||||||
|
<a href="mailto:registrar@dotgov.gov">registrar@dotgov.gov</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</address>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<div class="usa-identifier">
|
||||||
|
<section
|
||||||
|
class="usa-identifier__section usa-identifier__section--masthead"
|
||||||
|
aria-label="Agency identifier"
|
||||||
|
>
|
||||||
|
<div class="usa-identifier__container">
|
||||||
|
<div class="usa-identifier__logos">
|
||||||
|
<a href="https://www.cisa.gov" class="usa-identifier__logo"
|
||||||
|
><img
|
||||||
|
class="usa-identifier__logo-img"
|
||||||
|
src="{% static 'img/CISA_logo.png' %}"
|
||||||
|
alt="CISA logo"
|
||||||
|
role="img"
|
||||||
|
width="200px"
|
||||||
|
/></a>
|
||||||
|
</div>
|
||||||
|
<section
|
||||||
|
class="usa-identifier__identity"
|
||||||
|
aria-label="Agency description"
|
||||||
|
>
|
||||||
|
<p class="usa-identifier__identity-domain">get.gov</p>
|
||||||
|
<p class="usa-identifier__identity-disclaimer">
|
||||||
|
An official website of the <a href="https://www.cisa.gov" class="usa-link usa-link--external">Cybersecurity and Infrastructure Security Agency</a>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<nav
|
||||||
|
class="usa-identifier__section usa-identifier__section--required-links"
|
||||||
|
aria-label="Important links"
|
||||||
|
>
|
||||||
|
<div class="usa-identifier__container">
|
||||||
|
<ul class="usa-identifier__required-links-list">
|
||||||
|
<li class="usa-identifier__required-links-item">
|
||||||
|
<a
|
||||||
|
href="https://home.dotgov.gov/about/"
|
||||||
|
class="usa-identifier__required-link usa-link"
|
||||||
|
>About .gov</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="usa-identifier__required-links-item">
|
||||||
|
<a
|
||||||
|
href="https://github.com/cisagov/getgov"
|
||||||
|
class="usa-identifier__required-link usa-link usa-link--external"
|
||||||
|
>.gov on Github</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="usa-identifier__required-links-item">
|
||||||
|
<a href="https://home.dotgov.gov/privacy/" 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"
|
||||||
|
>Accessibility</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="usa-identifier__required-links-item">
|
||||||
|
<a href="TODO" 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"
|
||||||
|
>No FEAR Act data</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="usa-identifier__required-links-item">
|
||||||
|
<a href="https://www.dhs.gov/freedom-information-act-foia" class="usa-identifier__required-link usa-link"
|
||||||
|
>FOIA requests</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<section
|
||||||
|
class="usa-identifier__section usa-identifier__section--usagov"
|
||||||
|
aria-label="U.S. government information and services"
|
||||||
|
>
|
||||||
|
<div class="usa-identifier__container">
|
||||||
|
<div class="usa-identifier__usagov-description">
|
||||||
|
Looking for U.S. government information and services?
|
||||||
|
</div>
|
||||||
|
<a href="https://www.usa.gov/" class="usa-link">Visit USA.gov</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
|
@ -1,7 +1,57 @@
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model, login
|
from django.contrib.auth import get_user_model, login
|
||||||
|
|
||||||
|
|
||||||
|
def get_handlers():
|
||||||
|
"""Obtain pointers to all StreamHandlers."""
|
||||||
|
handlers = {}
|
||||||
|
|
||||||
|
rootlogger = logging.getLogger()
|
||||||
|
for h in rootlogger.handlers:
|
||||||
|
if isinstance(h, logging.StreamHandler):
|
||||||
|
handlers[h.name] = h
|
||||||
|
|
||||||
|
for logger in logging.Logger.manager.loggerDict.values():
|
||||||
|
if not isinstance(logger, logging.PlaceHolder):
|
||||||
|
for h in logger.handlers:
|
||||||
|
if isinstance(h, logging.StreamHandler):
|
||||||
|
handlers[h.name] = h
|
||||||
|
|
||||||
|
return handlers
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def less_console_noise():
|
||||||
|
"""
|
||||||
|
Context manager to use in tests to silence console logging.
|
||||||
|
|
||||||
|
This is helpful on tests which trigger console messages
|
||||||
|
(such as errors) which are normal and expected.
|
||||||
|
|
||||||
|
It can easily be removed to debug a failing test.
|
||||||
|
"""
|
||||||
|
restore = {}
|
||||||
|
handlers = get_handlers()
|
||||||
|
devnull = open(os.devnull, "w")
|
||||||
|
|
||||||
|
# redirect all the streams
|
||||||
|
for handler in handlers.values():
|
||||||
|
prior = handler.setStream(devnull)
|
||||||
|
restore[handler.name] = prior
|
||||||
|
try:
|
||||||
|
# run the test
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
# restore the streams
|
||||||
|
for handler in handlers.values():
|
||||||
|
handler.setStream(restore[handler.name])
|
||||||
|
|
||||||
|
|
||||||
class MockUserLogin:
|
class MockUserLogin:
|
||||||
def __init__(self, get_response):
|
def __init__(self, get_response):
|
||||||
self.get_response = get_response
|
self.get_response = get_response
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
|
|
||||||
from registrar.models import Contact, DomainApplication, User, Website
|
from registrar.models import Contact, DomainApplication, User, Website, Domain
|
||||||
|
from unittest import skip
|
||||||
|
|
||||||
|
|
||||||
class TestDomainApplication(TestCase):
|
class TestDomainApplication(TestCase):
|
||||||
|
@ -22,6 +23,7 @@ class TestDomainApplication(TestCase):
|
||||||
contact = Contact.objects.create()
|
contact = Contact.objects.create()
|
||||||
com_website, _ = Website.objects.get_or_create(website="igorville.com")
|
com_website, _ = Website.objects.get_or_create(website="igorville.com")
|
||||||
gov_website, _ = Website.objects.get_or_create(website="igorville.gov")
|
gov_website, _ = Website.objects.get_or_create(website="igorville.gov")
|
||||||
|
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||||
application = DomainApplication.objects.create(
|
application = DomainApplication.objects.create(
|
||||||
creator=user,
|
creator=user,
|
||||||
investigator=user,
|
investigator=user,
|
||||||
|
@ -35,7 +37,7 @@ class TestDomainApplication(TestCase):
|
||||||
state_territory="CA",
|
state_territory="CA",
|
||||||
zip_code="12345-6789",
|
zip_code="12345-6789",
|
||||||
authorizing_official=contact,
|
authorizing_official=contact,
|
||||||
requested_domain=gov_website,
|
requested_domain=domain,
|
||||||
submitter=contact,
|
submitter=contact,
|
||||||
purpose="Igorville rules!",
|
purpose="Igorville rules!",
|
||||||
security_email="security@igorville.gov",
|
security_email="security@igorville.gov",
|
||||||
|
@ -56,9 +58,101 @@ class TestDomainApplication(TestCase):
|
||||||
|
|
||||||
def test_status_fsm_submit_succeed(self):
|
def test_status_fsm_submit_succeed(self):
|
||||||
user, _ = User.objects.get_or_create()
|
user, _ = User.objects.get_or_create()
|
||||||
site = Website.objects.create(website="igorville.gov")
|
site = Domain.objects.create(name="igorville.gov")
|
||||||
application = DomainApplication.objects.create(
|
application = DomainApplication.objects.create(
|
||||||
creator=user, requested_domain=site
|
creator=user, requested_domain=site
|
||||||
)
|
)
|
||||||
application.submit()
|
application.submit()
|
||||||
self.assertEqual(application.status, application.SUBMITTED)
|
self.assertEqual(application.status, application.SUBMITTED)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDomain(TestCase):
|
||||||
|
def test_empty_create_fails(self):
|
||||||
|
"""Can't create a completely empty domain."""
|
||||||
|
with self.assertRaisesRegex(IntegrityError, "name"):
|
||||||
|
Domain.objects.create()
|
||||||
|
|
||||||
|
def test_minimal_create(self):
|
||||||
|
"""Can create with just a name."""
|
||||||
|
domain = Domain.objects.create(name="igorville.gov")
|
||||||
|
self.assertEquals(domain.is_active, False)
|
||||||
|
|
||||||
|
def test_get_status(self):
|
||||||
|
"""Returns proper status based on `is_active`."""
|
||||||
|
domain = Domain.objects.create(name="igorville.gov")
|
||||||
|
domain.save()
|
||||||
|
self.assertEquals(None, domain.status)
|
||||||
|
domain.activate()
|
||||||
|
domain.save()
|
||||||
|
self.assertIn("ok", domain.status)
|
||||||
|
|
||||||
|
def test_fsm_activate_fail_unique(self):
|
||||||
|
"""Can't activate domain if name is not unique."""
|
||||||
|
d1, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||||
|
d2, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||||
|
d1.activate()
|
||||||
|
d1.save()
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
d2.activate()
|
||||||
|
|
||||||
|
def test_fsm_activate_fail_unapproved(self):
|
||||||
|
"""Can't activate domain if application isn't approved."""
|
||||||
|
d1, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||||
|
user, _ = User.objects.get_or_create()
|
||||||
|
application = DomainApplication.objects.create(creator=user)
|
||||||
|
d1.domain_application = application
|
||||||
|
d1.save()
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
d1.activate()
|
||||||
|
|
||||||
|
|
||||||
|
@skip("Not implemented yet.")
|
||||||
|
class TestDomainApplicationLifeCycle(TestCase):
|
||||||
|
def test_application_approval(self):
|
||||||
|
# DomainApplication is created
|
||||||
|
# test: Domain is created and is inactive
|
||||||
|
# analyst approves DomainApplication
|
||||||
|
# test: Domain is activated
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_application_rejection(self):
|
||||||
|
# DomainApplication is created
|
||||||
|
# test: Domain is created and is inactive
|
||||||
|
# analyst rejects DomainApplication
|
||||||
|
# test: Domain remains inactive
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_application_deleted_before_approval(self):
|
||||||
|
# DomainApplication is created
|
||||||
|
# test: Domain is created and is inactive
|
||||||
|
# admin deletes DomainApplication
|
||||||
|
# test: Domain is deleted; Hosts, HostIps and Nameservers are deleted
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_application_deleted_following_approval(self):
|
||||||
|
# DomainApplication is created
|
||||||
|
# test: Domain is created and is inactive
|
||||||
|
# analyst approves DomainApplication
|
||||||
|
# admin deletes DomainApplication
|
||||||
|
# test: DomainApplication foreign key field on Domain is set to null
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_application_approval_with_conflicting_name(self):
|
||||||
|
# DomainApplication #1 is created
|
||||||
|
# test: Domain #1 is created and is inactive
|
||||||
|
# analyst approves DomainApplication #1
|
||||||
|
# test: Domain #1 is activated
|
||||||
|
# DomainApplication #2 is created, with the same domain name string
|
||||||
|
# test: Domain #2 is created and is inactive
|
||||||
|
# analyst approves DomainApplication #2
|
||||||
|
# test: error is raised
|
||||||
|
# test: DomainApplication #1 remains approved
|
||||||
|
# test: Domain #1 remains active
|
||||||
|
# test: DomainApplication #2 remains in investigating
|
||||||
|
# test: Domain #2 remains inactive
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_application_approval_with_network_errors(self):
|
||||||
|
# TODO: scenario wherein application is approved,
|
||||||
|
# but attempts to contact the registry to activate the domain fail
|
||||||
|
pass
|
||||||
|
|
|
@ -5,7 +5,10 @@ from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
from django_webtest import WebTest # type: ignore
|
from django_webtest import WebTest # type: ignore
|
||||||
|
|
||||||
from registrar.models import DomainApplication
|
from registrar.models import DomainApplication, Domain
|
||||||
|
from registrar.forms.application_wizard import TITLES
|
||||||
|
|
||||||
|
from .common import less_console_noise
|
||||||
|
|
||||||
|
|
||||||
class TestViews(TestCase):
|
class TestViews(TestCase):
|
||||||
|
@ -54,6 +57,18 @@ class LoggedInTests(TestWithUser):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
def test_home_lists_domain_applications(self):
|
||||||
|
response = self.client.get("/")
|
||||||
|
self.assertNotContains(response, "igorville.gov")
|
||||||
|
site = Domain.objects.create(name="igorville.gov")
|
||||||
|
application = DomainApplication.objects.create(
|
||||||
|
creator=self.user, requested_domain=site
|
||||||
|
)
|
||||||
|
response = self.client.get("/")
|
||||||
|
self.assertContains(response, "igorville.gov", count=1)
|
||||||
|
# clean up
|
||||||
|
application.delete()
|
||||||
|
|
||||||
def test_whoami_page(self):
|
def test_whoami_page(self):
|
||||||
"""User information appears on the whoami page."""
|
"""User information appears on the whoami page."""
|
||||||
response = self.client.get("/whoami/")
|
response = self.client.get("/whoami/")
|
||||||
|
@ -96,15 +111,6 @@ class FormTests(TestWithUser, WebTest):
|
||||||
result = page.form.submit()
|
result = page.form.submit()
|
||||||
self.assertIn("What kind of government organization do you represent?", result)
|
self.assertIn("What kind of government organization do you represent?", result)
|
||||||
|
|
||||||
def test_application_form_organization(self):
|
|
||||||
# 302 redirect to the first form
|
|
||||||
page = self.app.get(reverse("application")).follow()
|
|
||||||
form = page.form
|
|
||||||
form["organization_type-organization_type"] = "Federal"
|
|
||||||
result = page.form.submit().follow()
|
|
||||||
# Got the next form page
|
|
||||||
self.assertContains(result, "contact information")
|
|
||||||
|
|
||||||
def test_application_form_submission(self):
|
def test_application_form_submission(self):
|
||||||
"""Can fill out the entire form and submit.
|
"""Can fill out the entire form and submit.
|
||||||
As we add additional form pages, we need to include them here to make
|
As we add additional form pages, we need to include them here to make
|
||||||
|
@ -130,10 +136,9 @@ class FormTests(TestWithUser, WebTest):
|
||||||
self.assertEquals(type_result.status_code, 302)
|
self.assertEquals(type_result.status_code, 302)
|
||||||
self.assertEquals(type_result["Location"], "/register/organization_federal/")
|
self.assertEquals(type_result["Location"], "/register/organization_federal/")
|
||||||
|
|
||||||
# TODO: In the future this should be conditionally dispalyed based on org type
|
|
||||||
|
|
||||||
# ---- FEDERAL BRANCH PAGE ----
|
# ---- FEDERAL BRANCH PAGE ----
|
||||||
# Follow the redirect to the next form page
|
# Follow the redirect to the next form page
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
federal_page = type_result.follow()
|
federal_page = type_result.follow()
|
||||||
federal_form = federal_page.form
|
federal_form = federal_page.form
|
||||||
federal_form["organization_federal-federal_type"] = "Executive"
|
federal_form["organization_federal-federal_type"] = "Executive"
|
||||||
|
@ -143,27 +148,11 @@ class FormTests(TestWithUser, WebTest):
|
||||||
federal_result = federal_form.submit()
|
federal_result = federal_form.submit()
|
||||||
|
|
||||||
self.assertEquals(federal_result.status_code, 302)
|
self.assertEquals(federal_result.status_code, 302)
|
||||||
self.assertEquals(
|
self.assertEquals(federal_result["Location"], "/register/organization_contact/")
|
||||||
federal_result["Location"], "/register/organization_election/"
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---- ELECTION BOARD BRANCH PAGE ----
|
|
||||||
# Follow the redirect to the next form page
|
|
||||||
election_page = federal_result.follow()
|
|
||||||
election_form = election_page.form
|
|
||||||
election_form["organization_election-is_election_board"] = True
|
|
||||||
|
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
|
||||||
election_result = election_form.submit()
|
|
||||||
|
|
||||||
self.assertEquals(election_result.status_code, 302)
|
|
||||||
self.assertEquals(
|
|
||||||
election_result["Location"], "/register/organization_contact/"
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---- ORG CONTACT PAGE ----
|
# ---- ORG CONTACT PAGE ----
|
||||||
# Follow the redirect to the next form page
|
# Follow the redirect to the next form page
|
||||||
org_contact_page = election_result.follow()
|
org_contact_page = federal_result.follow()
|
||||||
org_contact_form = org_contact_page.form
|
org_contact_form = org_contact_page.form
|
||||||
org_contact_form["organization_contact-organization_name"] = "Testorg"
|
org_contact_form["organization_contact-organization_name"] = "Testorg"
|
||||||
org_contact_form["organization_contact-address_line1"] = "address 1"
|
org_contact_form["organization_contact-address_line1"] = "address 1"
|
||||||
|
@ -320,5 +309,72 @@ class FormTests(TestWithUser, WebTest):
|
||||||
# following this redirect is a GET request, so include the cookie
|
# following this redirect is a GET request, so include the cookie
|
||||||
# here too.
|
# here too.
|
||||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
final_result = review_result.follow()
|
with less_console_noise():
|
||||||
|
final_result = review_result.follow()
|
||||||
self.assertContains(final_result, "Thank you for your domain request")
|
self.assertContains(final_result, "Thank you for your domain request")
|
||||||
|
|
||||||
|
def test_application_form_conditional_federal(self):
|
||||||
|
"""Federal branch question is shown for federal organizations."""
|
||||||
|
type_page = self.app.get(reverse("application")).follow()
|
||||||
|
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||||
|
# resetting the session key on each new request, thus destroying the concept
|
||||||
|
# of a "session". We are going to do it manually, saving the session ID here
|
||||||
|
# and then setting the cookie on each request.
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
|
||||||
|
# ---- TYPE PAGE ----
|
||||||
|
|
||||||
|
# the conditional step titles shouldn't appear initially
|
||||||
|
self.assertNotContains(type_page, TITLES["organization_federal"])
|
||||||
|
self.assertNotContains(type_page, TITLES["organization_election"])
|
||||||
|
type_form = type_page.form
|
||||||
|
type_form["organization_type-organization_type"] = "Federal"
|
||||||
|
|
||||||
|
# set the session ID before .submit()
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
type_result = type_form.submit()
|
||||||
|
|
||||||
|
# the post request should return a redirect to the federal branch
|
||||||
|
# question
|
||||||
|
self.assertEquals(type_result.status_code, 302)
|
||||||
|
self.assertEquals(type_result["Location"], "/register/organization_federal/")
|
||||||
|
|
||||||
|
# and the step label should appear in the sidebar of the resulting page
|
||||||
|
# but the step label for the elections page should not appear
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
federal_page = type_result.follow()
|
||||||
|
self.assertContains(federal_page, TITLES["organization_federal"])
|
||||||
|
self.assertNotContains(federal_page, TITLES["organization_election"])
|
||||||
|
|
||||||
|
def test_application_form_conditional_elections(self):
|
||||||
|
"""Election question is shown for other organizations."""
|
||||||
|
type_page = self.app.get(reverse("application")).follow()
|
||||||
|
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||||
|
# resetting the session key on each new request, thus destroying the concept
|
||||||
|
# of a "session". We are going to do it manually, saving the session ID here
|
||||||
|
# and then setting the cookie on each request.
|
||||||
|
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||||
|
|
||||||
|
# ---- TYPE PAGE ----
|
||||||
|
|
||||||
|
# the conditional step titles shouldn't appear initially
|
||||||
|
self.assertNotContains(type_page, TITLES["organization_federal"])
|
||||||
|
self.assertNotContains(type_page, TITLES["organization_election"])
|
||||||
|
type_form = type_page.form
|
||||||
|
type_form["organization_type-organization_type"] = "County"
|
||||||
|
|
||||||
|
# set the session ID before .submit()
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
type_result = type_form.submit()
|
||||||
|
|
||||||
|
# the post request should return a redirect to the federal branch
|
||||||
|
# question
|
||||||
|
self.assertEquals(type_result.status_code, 302)
|
||||||
|
self.assertEquals(type_result["Location"], "/register/organization_election/")
|
||||||
|
|
||||||
|
# and the step label should appear in the sidebar of the resulting page
|
||||||
|
# but the step label for the elections page should not appear
|
||||||
|
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||||
|
election_page = type_result.follow()
|
||||||
|
self.assertContains(election_page, TITLES["organization_election"])
|
||||||
|
self.assertNotContains(election_page, TITLES["organization_federal"])
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
from registrar.models import DomainApplication
|
||||||
|
|
||||||
|
|
||||||
def index(request):
|
def index(request):
|
||||||
"""This page is available to anyone without logging in."""
|
"""This page is available to anyone without logging in."""
|
||||||
return render(request, "home.html")
|
context = {}
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
applications = DomainApplication.objects.filter(creator=request.user)
|
||||||
|
context["domain_applications"] = applications
|
||||||
|
return render(request, "home.html", context)
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
10038 OUTOFSCOPE http://app:8080/public/img/.*
|
10038 OUTOFSCOPE http://app:8080/public/img/.*
|
||||||
10038 OUTOFSCOPE http://app:8080/public/css/.*
|
10038 OUTOFSCOPE http://app:8080/public/css/.*
|
||||||
10038 OUTOFSCOPE http://app:8080/public/js/.*
|
10038 OUTOFSCOPE http://app:8080/public/js/.*
|
||||||
10038 OUTOFSCOPE http://app:8080/(robots.txt|sitemap.xml)
|
10038 OUTOFSCOPE http://app:8080/(robots.txt|sitemap.xml|TODO)
|
||||||
# OIDC isn't configured in the test environment and DEBUG=True so this gives a 500 without CSP headers
|
# 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/login/
|
||||||
10039 FAIL (X-Backend-Server Header Information Leak - Passive/beta)
|
10039 FAIL (X-Backend-Server Header Information Leak - Passive/beta)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue