mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-24 11:38:39 +02:00
Merge branch 'main' of https://github.com/cisagov/getgov into rh/687-formatting-nameservers
This commit is contained in:
commit
30dc1d1a37
52 changed files with 2501 additions and 1511 deletions
18
.github/ISSUE_TEMPLATE/issue-default.yml
vendored
18
.github/ISSUE_TEMPLATE/issue-default.yml
vendored
|
@ -6,13 +6,13 @@ body:
|
|||
id: title-help
|
||||
attributes:
|
||||
value: |
|
||||
> Titles should be short, descriptive, and compelling.
|
||||
> Titles should be short, descriptive, and compelling. Use sentence case.
|
||||
- type: textarea
|
||||
id: issue-description
|
||||
attributes:
|
||||
label: Issue description and context
|
||||
label: Issue description
|
||||
description: |
|
||||
Describe the issue so that someone who wasn't present for its discovery can understand the problem and why it matters. Use full sentences, plain language, and good [formatting](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax). Share desired outcomes or potential next steps. Images or links to other content/context (like documents or Slack discussions) are welcome.
|
||||
Describe the issue so that someone who wasn't present for its discovery can understand why it matters. Use full sentences, plain language, and [good formatting](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax).
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
@ -20,16 +20,22 @@ body:
|
|||
attributes:
|
||||
label: Acceptance criteria
|
||||
description: "If known, share 1-3 statements that would need to be true for this issue to be considered resolved. Use a [task list](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/about-task-lists#creating-task-lists) if appropriate."
|
||||
placeholder: "- [ ] The button does the thing."
|
||||
placeholder: "- [ ]"
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: "Share any other thoughts, like how this might be implemented or fixed. Screenshots and links to documents/discussions are welcome."
|
||||
- type: textarea
|
||||
id: links-to-other-issues
|
||||
attributes:
|
||||
label: Links to other issues
|
||||
description: |
|
||||
Add the issue #number of other issues this relates to and how (e.g., 🚧 Blocks, ⛔️ Is blocked by, 🔄 Relates to).
|
||||
"Add issue #numbers this relates to and how (e.g., 🚧 [construction] Blocks, ⛔️ [no_entry] Is blocked by, 🔄 [arrows_counterclockwise] Relates to)."
|
||||
placeholder: 🔄 Relates to...
|
||||
- type: markdown
|
||||
id: note
|
||||
attributes:
|
||||
value: |
|
||||
> We may edit this issue's text to document our understanding and clarify the product work.
|
||||
> We may edit the text in this issue to document our understanding and clarify the product work.
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ Date: 2023-13-10
|
|||
|
||||
## Status
|
||||
|
||||
In Review
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ applications:
|
|||
# Tell Django where to find its configuration
|
||||
DJANGO_SETTINGS_MODULE: registrar.config.settings
|
||||
# Tell Django where it is being hosted
|
||||
DJANGO_BASE_URL: https://getgov-stable.app.cloud.gov
|
||||
DJANGO_BASE_URL: https://manage.get.gov
|
||||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# default public site location
|
||||
|
|
|
@ -25,7 +25,10 @@ django-phonenumber-field = {extras = ["phonenumberslite"], version = "*"}
|
|||
boto3 = "*"
|
||||
typing-extensions ='*'
|
||||
django-login-required-middleware = "*"
|
||||
greenlet = "*"
|
||||
gevent = "*"
|
||||
fred-epplib = {git = "https://github.com/cisagov/epplib.git", ref = "master"}
|
||||
geventconnpool = {git = "https://github.com/rasky/geventconnpool.git", ref = "1bbb93a714a331a069adf27265fe582d9ba7ecd4"}
|
||||
|
||||
[dev-packages]
|
||||
django-debug-toolbar = "*"
|
||||
|
|
1707
src/Pipfile.lock
generated
1707
src/Pipfile.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,7 +1,10 @@
|
|||
"""Provide a wrapper around epplib to handle authentication and errors."""
|
||||
|
||||
import logging
|
||||
|
||||
from time import sleep
|
||||
from gevent import Timeout
|
||||
from epplibwrapper.utility.pool_status import PoolStatus
|
||||
|
||||
try:
|
||||
from epplib.client import Client
|
||||
|
@ -16,6 +19,7 @@ from django.conf import settings
|
|||
from .cert import Cert, Key
|
||||
from .errors import LoginError, RegistryError
|
||||
from .socket import Socket
|
||||
from .utility.pool import EPPConnectionPool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -39,9 +43,8 @@ class EPPLibWrapper:
|
|||
ATTN: This should not be used directly. Use `Domain` from domain.py.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, start_connection_pool=True) -> None:
|
||||
"""Initialize settings which will be used for all connections."""
|
||||
|
||||
# prepare (but do not send) a Login command
|
||||
self._login = commands.Login(
|
||||
cl_id=settings.SECRET_REGISTRY_CL_ID,
|
||||
|
@ -51,6 +54,7 @@ class EPPLibWrapper:
|
|||
"urn:ietf:params:xml:ns:contact-1.0",
|
||||
],
|
||||
)
|
||||
|
||||
# establish a client object with a TCP socket transport
|
||||
self._client = Client(
|
||||
SocketTransport(
|
||||
|
@ -60,37 +64,77 @@ class EPPLibWrapper:
|
|||
password=settings.SECRET_REGISTRY_KEY_PASSPHRASE,
|
||||
)
|
||||
)
|
||||
# prepare a context manager which will connect and login when invoked
|
||||
# (it will also logout and disconnect when the context manager exits)
|
||||
self._connect = Socket(self._client, self._login)
|
||||
|
||||
self.pool_options = {
|
||||
# Pool size
|
||||
"size": settings.EPP_CONNECTION_POOL_SIZE,
|
||||
# Which errors the pool should look out for.
|
||||
# Avoid changing this unless necessary,
|
||||
# it can and will break things.
|
||||
"exc_classes": (TransportError,),
|
||||
# Occasionally pings the registry to keep the connection alive.
|
||||
# Value in seconds => (keepalive / size)
|
||||
"keepalive": settings.POOL_KEEP_ALIVE,
|
||||
}
|
||||
|
||||
self._pool = None
|
||||
|
||||
# Tracks the status of the pool
|
||||
self.pool_status = PoolStatus()
|
||||
|
||||
if start_connection_pool:
|
||||
self.start_connection_pool()
|
||||
|
||||
def _send(self, command):
|
||||
"""Helper function used by `send`."""
|
||||
cmd_type = command.__class__.__name__
|
||||
|
||||
# Start a timeout to check if the pool is hanging
|
||||
timeout = Timeout(settings.POOL_TIMEOUT)
|
||||
timeout.start()
|
||||
|
||||
try:
|
||||
cmd_type = command.__class__.__name__
|
||||
with self._connect as wire:
|
||||
response = wire.send(command)
|
||||
if not self.pool_status.connection_success:
|
||||
raise LoginError(
|
||||
"Couldn't connect to the registry after three attempts"
|
||||
)
|
||||
with self._pool.get() as connection:
|
||||
response = connection.send(command)
|
||||
except Timeout as t:
|
||||
# If more than one pool exists,
|
||||
# multiple timeouts can be floating around.
|
||||
# We need to be specific as to which we are targeting.
|
||||
if t is timeout:
|
||||
# Flag that the pool is frozen,
|
||||
# then restart the pool.
|
||||
self.pool_status.pool_hanging = True
|
||||
self.start_connection_pool()
|
||||
except (ValueError, ParsingError) as err:
|
||||
message = "%s failed to execute due to some syntax error."
|
||||
logger.warning(message, cmd_type, exc_info=True)
|
||||
message = f"{cmd_type} failed to execute due to some syntax error."
|
||||
logger.error(f"{message} Error: {err}", exc_info=True)
|
||||
raise RegistryError(message) from err
|
||||
except TransportError as err:
|
||||
message = "%s failed to execute due to a connection error."
|
||||
logger.warning(message, cmd_type, exc_info=True)
|
||||
message = f"{cmd_type} failed to execute due to a connection error."
|
||||
logger.error(f"{message} Error: {err}", exc_info=True)
|
||||
raise RegistryError(message) from err
|
||||
except LoginError as err:
|
||||
message = "%s failed to execute due to a registry login error."
|
||||
logger.warning(message, cmd_type, exc_info=True)
|
||||
# For linter due to it not liking this line length
|
||||
text = "failed to execute due to a registry login error."
|
||||
message = f"{cmd_type} {text}"
|
||||
logger.error(f"{message} Error: {err}", exc_info=True)
|
||||
raise RegistryError(message) from err
|
||||
except Exception as err:
|
||||
message = "%s failed to execute due to an unknown error." % err
|
||||
logger.warning(message, cmd_type, exc_info=True)
|
||||
message = f"{cmd_type} failed to execute due to an unknown error."
|
||||
logger.error(f"{message} Error: {err}", exc_info=True)
|
||||
raise RegistryError(message) from err
|
||||
else:
|
||||
if response.code >= 2000:
|
||||
raise RegistryError(response.msg, code=response.code)
|
||||
else:
|
||||
return response
|
||||
finally:
|
||||
# Close the timeout no matter what happens
|
||||
timeout.close()
|
||||
|
||||
def send(self, command, *, cleaned=False):
|
||||
"""Login, send the command, then close the connection. Tries 3 times."""
|
||||
|
@ -98,6 +142,23 @@ class EPPLibWrapper:
|
|||
if not cleaned:
|
||||
raise ValueError("Please sanitize user input before sending it.")
|
||||
|
||||
# Reopen the pool if its closed
|
||||
# Only occurs when a login error is raised, after connection is successful
|
||||
if not self.pool_status.pool_running:
|
||||
# We want to reopen the connection pool,
|
||||
# but we don't want the end user to wait while it opens.
|
||||
# Raise syntax doesn't allow this, so we use a try/catch
|
||||
# block.
|
||||
try:
|
||||
logger.error("Can't contact the Registry. Pool was not running.")
|
||||
raise RegistryError("Can't contact the Registry. Pool was not running.")
|
||||
except RegistryError as err:
|
||||
raise err
|
||||
finally:
|
||||
# Code execution will halt after here.
|
||||
# The end user will need to recall .send.
|
||||
self.start_connection_pool()
|
||||
|
||||
counter = 0 # we'll try 3 times
|
||||
while True:
|
||||
try:
|
||||
|
@ -109,11 +170,73 @@ class EPPLibWrapper:
|
|||
else: # don't try again
|
||||
raise err
|
||||
|
||||
def get_pool(self):
|
||||
"""Get the current pool instance"""
|
||||
return self._pool
|
||||
|
||||
def _create_pool(self, client, login, options):
|
||||
"""Creates and returns new pool instance"""
|
||||
return EPPConnectionPool(client, login, options)
|
||||
|
||||
def start_connection_pool(self, restart_pool_if_exists=True):
|
||||
"""Starts a connection pool for the registry.
|
||||
|
||||
restart_pool_if_exists -> bool:
|
||||
If an instance of the pool already exists,
|
||||
then then that instance will be killed first.
|
||||
It is generally recommended to keep this enabled.
|
||||
"""
|
||||
# Since we reuse the same creds for each pool, we can test on
|
||||
# one socket, and if successful, then we know we can connect.
|
||||
if not self._test_registry_connection_success():
|
||||
logger.warning("Cannot contact the Registry")
|
||||
self.pool_status.connection_success = False
|
||||
else:
|
||||
self.pool_status.connection_success = True
|
||||
|
||||
# If this function is reinvoked, then ensure
|
||||
# that we don't have duplicate data sitting around.
|
||||
if self._pool is not None and restart_pool_if_exists:
|
||||
logger.info("Connection pool restarting...")
|
||||
self.kill_pool()
|
||||
|
||||
self._pool = self._create_pool(self._client, self._login, self.pool_options)
|
||||
|
||||
self.pool_status.pool_running = True
|
||||
self.pool_status.pool_hanging = False
|
||||
|
||||
logger.info("Connection pool started")
|
||||
|
||||
def kill_pool(self):
|
||||
"""Kills the existing pool. Use this instead
|
||||
of self._pool = None, as that doesn't clear
|
||||
gevent instances."""
|
||||
if self._pool is not None:
|
||||
self._pool.kill_all_connections()
|
||||
self._pool = None
|
||||
self.pool_status.pool_running = False
|
||||
return None
|
||||
logger.info("kill_pool() was invoked but there was no pool to delete")
|
||||
|
||||
def _test_registry_connection_success(self):
|
||||
"""Check that determines if our login
|
||||
credentials are valid, and/or if the Registrar
|
||||
can be contacted
|
||||
"""
|
||||
socket = Socket(self._client, self._login)
|
||||
can_login = False
|
||||
|
||||
# Something went wrong if this doesn't exist
|
||||
if hasattr(socket, "test_connection_success"):
|
||||
can_login = socket.test_connection_success()
|
||||
|
||||
return can_login
|
||||
|
||||
|
||||
try:
|
||||
# Initialize epplib
|
||||
CLIENT = EPPLibWrapper()
|
||||
logger.debug("registry client initialized")
|
||||
logger.info("registry client initialized")
|
||||
except Exception:
|
||||
CLIENT = None # type: ignore
|
||||
logger.warning(
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import logging
|
||||
from time import sleep
|
||||
|
||||
try:
|
||||
from epplib import commands
|
||||
from epplib.client import Client
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
@ -14,24 +16,84 @@ logger = logging.getLogger(__name__)
|
|||
class Socket:
|
||||
"""Context manager which establishes a TCP connection with registry."""
|
||||
|
||||
def __init__(self, client, login) -> None:
|
||||
def __init__(self, client: Client, login: commands.Login) -> None:
|
||||
"""Save the epplib client and login details."""
|
||||
self.client = client
|
||||
self.login = login
|
||||
|
||||
def __enter__(self):
|
||||
"""Runs connect(), which opens a connection with EPPLib."""
|
||||
self.connect()
|
||||
|
||||
def __exit__(self, *args, **kwargs):
|
||||
"""Runs disconnect(), which closes a connection with EPPLib."""
|
||||
self.disconnect()
|
||||
|
||||
def connect(self):
|
||||
"""Use epplib to connect."""
|
||||
self.client.connect()
|
||||
response = self.client.send(self.login)
|
||||
if response.code >= 2000:
|
||||
if self.is_login_error(response.code):
|
||||
self.client.close()
|
||||
raise LoginError(response.msg)
|
||||
return self.client
|
||||
|
||||
def __exit__(self, *args, **kwargs):
|
||||
def disconnect(self):
|
||||
"""Close the connection."""
|
||||
try:
|
||||
self.client.send(commands.Logout())
|
||||
self.client.close()
|
||||
except Exception:
|
||||
logger.warning("Connection to registry was not cleanly closed.")
|
||||
|
||||
def send(self, command):
|
||||
"""Sends a command to the registry.
|
||||
If the RegistryError code is >= 2000,
|
||||
then this function raises a LoginError.
|
||||
The calling function should handle this."""
|
||||
response = self.client.send(command)
|
||||
if self.is_login_error(response.code):
|
||||
self.client.close()
|
||||
raise LoginError(response.msg)
|
||||
|
||||
return response
|
||||
|
||||
def is_login_error(self, code):
|
||||
"""Returns the result of code >= 2000 for RegistryError.
|
||||
This indicates that something weird happened on the Registry,
|
||||
and that we should return a LoginError."""
|
||||
return code >= 2000
|
||||
|
||||
def test_connection_success(self):
|
||||
"""Tests if a successful connection can be made with the registry.
|
||||
Tries 3 times."""
|
||||
# Something went wrong if this doesn't exist
|
||||
if not hasattr(self.client, "connect"):
|
||||
logger.warning("self.client does not have a connect attribute")
|
||||
return False
|
||||
|
||||
counter = 0 # we'll try 3 times
|
||||
while True:
|
||||
try:
|
||||
self.client.connect()
|
||||
response = self.client.send(self.login)
|
||||
except LoginError as err:
|
||||
if err.should_retry() and counter < 3:
|
||||
counter += 1
|
||||
sleep((counter * 50) / 1000) # sleep 50 ms to 150 ms
|
||||
else: # don't try again
|
||||
return False
|
||||
# Occurs when an invalid creds are passed in - such as on localhost
|
||||
except OSError as err:
|
||||
logger.error(err)
|
||||
return False
|
||||
else:
|
||||
self.disconnect()
|
||||
|
||||
# If we encounter a login error, fail
|
||||
if self.is_login_error(response.code):
|
||||
logger.warning("A login error was found in test_connection_success")
|
||||
return False
|
||||
|
||||
# Otherwise, just return true
|
||||
return True
|
||||
|
|
0
src/epplibwrapper/tests/__init__.py
Normal file
0
src/epplibwrapper/tests/__init__.py
Normal file
258
src/epplibwrapper/tests/test_pool.py
Normal file
258
src/epplibwrapper/tests/test_pool.py
Normal file
|
@ -0,0 +1,258 @@
|
|||
import datetime
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
from dateutil.tz import tzlocal # type: ignore
|
||||
from django.test import TestCase
|
||||
from epplibwrapper.client import EPPLibWrapper
|
||||
from epplibwrapper.errors import RegistryError
|
||||
from epplibwrapper.socket import Socket
|
||||
from epplibwrapper.utility.pool import EPPConnectionPool
|
||||
from registrar.models.domain import registry
|
||||
from contextlib import ExitStack
|
||||
|
||||
import logging
|
||||
|
||||
try:
|
||||
from epplib import commands
|
||||
from epplib.client import Client
|
||||
from epplib.exceptions import TransportError
|
||||
from epplib.transport import SocketTransport
|
||||
from epplib.models import common, info
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestConnectionPool(TestCase):
|
||||
"""Tests for our connection pooling behaviour"""
|
||||
|
||||
def setUp(self):
|
||||
# Mimic the settings added to settings.py
|
||||
self.pool_options = {
|
||||
# Current pool size
|
||||
"size": 1,
|
||||
# Which errors the pool should look out for
|
||||
"exc_classes": (TransportError,),
|
||||
# Occasionally pings the registry to keep the connection alive.
|
||||
# Value in seconds => (keepalive / size)
|
||||
"keepalive": 60,
|
||||
}
|
||||
|
||||
def fake_socket(self, login, client):
|
||||
# Linter reasons
|
||||
pw = "none"
|
||||
# Create a fake client object
|
||||
fake_client = Client(
|
||||
SocketTransport(
|
||||
"none",
|
||||
cert_file="path/to/cert_file",
|
||||
key_file="path/to/key_file",
|
||||
password=pw,
|
||||
)
|
||||
)
|
||||
|
||||
return Socket(fake_client, MagicMock())
|
||||
|
||||
def patch_success(self):
|
||||
return True
|
||||
|
||||
def fake_send(self, command, cleaned=None):
|
||||
mock = MagicMock(
|
||||
code=1000,
|
||||
msg="Command completed successfully",
|
||||
res_data=None,
|
||||
cl_tr_id="xkw1uo#2023-10-17T15:29:09.559376",
|
||||
sv_tr_id="5CcH4gxISuGkq8eqvr1UyQ==-35a",
|
||||
extensions=[],
|
||||
msg_q=None,
|
||||
)
|
||||
return mock
|
||||
|
||||
def fake_client(mock_client):
|
||||
pw = "none"
|
||||
client = Client(
|
||||
SocketTransport(
|
||||
"none",
|
||||
cert_file="path/to/cert_file",
|
||||
key_file="path/to/key_file",
|
||||
password=pw,
|
||||
)
|
||||
)
|
||||
return client
|
||||
|
||||
@patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success)
|
||||
def test_pool_sends_data(self):
|
||||
"""A .send is invoked on the pool successfully"""
|
||||
expected_result = {
|
||||
"cl_tr_id": None,
|
||||
"code": 1000,
|
||||
"extensions": [],
|
||||
"msg": "Command completed successfully",
|
||||
"msg_q": None,
|
||||
"res_data": [
|
||||
info.InfoDomainResultData(
|
||||
roid="DF1340360-GOV",
|
||||
statuses=[
|
||||
common.Status(
|
||||
state="serverTransferProhibited",
|
||||
description=None,
|
||||
lang="en",
|
||||
),
|
||||
common.Status(state="inactive", description=None, lang="en"),
|
||||
],
|
||||
cl_id="gov2023-ote",
|
||||
cr_id="gov2023-ote",
|
||||
cr_date=datetime.datetime(
|
||||
2023, 8, 15, 23, 56, 36, tzinfo=tzlocal()
|
||||
),
|
||||
up_id="gov2023-ote",
|
||||
up_date=datetime.datetime(2023, 8, 17, 2, 3, 19, tzinfo=tzlocal()),
|
||||
tr_date=None,
|
||||
name="test3.gov",
|
||||
registrant="TuaWnx9hnm84GCSU",
|
||||
admins=[],
|
||||
nsset=None,
|
||||
keyset=None,
|
||||
ex_date=datetime.date(2024, 8, 15),
|
||||
auth_info=info.DomainAuthInfo(pw="2fooBAR123fooBaz"),
|
||||
)
|
||||
],
|
||||
"sv_tr_id": "wRRNVhKhQW2m6wsUHbo/lA==-29a",
|
||||
}
|
||||
|
||||
# Mock a response from EPP
|
||||
def fake_receive(command, cleaned=None):
|
||||
location = Path(__file__).parent / "utility" / "infoDomain.xml"
|
||||
xml = (location).read_bytes()
|
||||
return xml
|
||||
|
||||
# Mock what happens inside the "with"
|
||||
with ExitStack() as stack:
|
||||
stack.enter_context(
|
||||
patch.object(EPPConnectionPool, "_create_socket", self.fake_socket)
|
||||
)
|
||||
stack.enter_context(patch.object(Socket, "connect", self.fake_client))
|
||||
stack.enter_context(patch.object(SocketTransport, "send", self.fake_send))
|
||||
stack.enter_context(patch.object(SocketTransport, "receive", fake_receive))
|
||||
# Restart the connection pool
|
||||
registry.start_connection_pool()
|
||||
# Pool should be running, and be the right size
|
||||
self.assertEqual(registry.pool_status.connection_success, True)
|
||||
self.assertEqual(registry.pool_status.pool_running, True)
|
||||
|
||||
# Send a command
|
||||
result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True)
|
||||
|
||||
# Should this ever fail, it either means that the schema has changed,
|
||||
# or the pool is broken.
|
||||
# If the schema has changed: Update the associated infoDomain.xml file
|
||||
self.assertEqual(result.__dict__, expected_result)
|
||||
|
||||
# The number of open pools should match the number of requested ones.
|
||||
# If it is 0, then they failed to open
|
||||
self.assertEqual(len(registry._pool.conn), self.pool_options["size"])
|
||||
|
||||
@patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success)
|
||||
def test_pool_restarts_on_send(self):
|
||||
"""A .send is invoked, but the pool isn't running.
|
||||
The pool should restart."""
|
||||
expected_result = {
|
||||
"cl_tr_id": None,
|
||||
"code": 1000,
|
||||
"extensions": [],
|
||||
"msg": "Command completed successfully",
|
||||
"msg_q": None,
|
||||
"res_data": [
|
||||
info.InfoDomainResultData(
|
||||
roid="DF1340360-GOV",
|
||||
statuses=[
|
||||
common.Status(
|
||||
state="serverTransferProhibited",
|
||||
description=None,
|
||||
lang="en",
|
||||
),
|
||||
common.Status(state="inactive", description=None, lang="en"),
|
||||
],
|
||||
cl_id="gov2023-ote",
|
||||
cr_id="gov2023-ote",
|
||||
cr_date=datetime.datetime(
|
||||
2023, 8, 15, 23, 56, 36, tzinfo=tzlocal()
|
||||
),
|
||||
up_id="gov2023-ote",
|
||||
up_date=datetime.datetime(2023, 8, 17, 2, 3, 19, tzinfo=tzlocal()),
|
||||
tr_date=None,
|
||||
name="test3.gov",
|
||||
registrant="TuaWnx9hnm84GCSU",
|
||||
admins=[],
|
||||
nsset=None,
|
||||
keyset=None,
|
||||
ex_date=datetime.date(2024, 8, 15),
|
||||
auth_info=info.DomainAuthInfo(pw="2fooBAR123fooBaz"),
|
||||
)
|
||||
],
|
||||
"sv_tr_id": "wRRNVhKhQW2m6wsUHbo/lA==-29a",
|
||||
}
|
||||
|
||||
# Mock a response from EPP
|
||||
def fake_receive(command, cleaned=None):
|
||||
location = Path(__file__).parent / "utility" / "infoDomain.xml"
|
||||
xml = (location).read_bytes()
|
||||
return xml
|
||||
|
||||
# Mock what happens inside the "with"
|
||||
with ExitStack() as stack:
|
||||
stack.enter_context(
|
||||
patch.object(EPPConnectionPool, "_create_socket", self.fake_socket)
|
||||
)
|
||||
stack.enter_context(patch.object(Socket, "connect", self.fake_client))
|
||||
stack.enter_context(patch.object(SocketTransport, "send", self.fake_send))
|
||||
stack.enter_context(patch.object(SocketTransport, "receive", fake_receive))
|
||||
# Kill the connection pool
|
||||
registry.kill_pool()
|
||||
|
||||
self.assertEqual(registry.pool_status.connection_success, False)
|
||||
self.assertEqual(registry.pool_status.pool_running, False)
|
||||
|
||||
# An exception should be raised as end user will be informed
|
||||
# that they cannot connect to EPP
|
||||
with self.assertRaises(RegistryError):
|
||||
expected = "InfoDomain failed to execute due to a connection error."
|
||||
result = registry.send(
|
||||
commands.InfoDomain(name="test.gov"), cleaned=True
|
||||
)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
# A subsequent command should be successful, as the pool restarts
|
||||
result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True)
|
||||
# Should this ever fail, it either means that the schema has changed,
|
||||
# or the pool is broken.
|
||||
# If the schema has changed: Update the associated infoDomain.xml file
|
||||
self.assertEqual(result.__dict__, expected_result)
|
||||
|
||||
# The number of open pools should match the number of requested ones.
|
||||
# If it is 0, then they failed to open
|
||||
self.assertEqual(len(registry._pool.conn), self.pool_options["size"])
|
||||
|
||||
@patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success)
|
||||
def test_raises_connection_error(self):
|
||||
"""A .send is invoked on the pool, but registry connection is lost
|
||||
right as we send a command."""
|
||||
|
||||
with ExitStack() as stack:
|
||||
stack.enter_context(
|
||||
patch.object(EPPConnectionPool, "_create_socket", self.fake_socket)
|
||||
)
|
||||
stack.enter_context(patch.object(Socket, "connect", self.fake_client))
|
||||
|
||||
# Pool should be running
|
||||
self.assertEqual(registry.pool_status.connection_success, True)
|
||||
self.assertEqual(registry.pool_status.pool_running, True)
|
||||
|
||||
# Try to send a command out - should fail
|
||||
with self.assertRaises(RegistryError):
|
||||
expected = "InfoDomain failed to execute due to a connection error."
|
||||
result = registry.send(
|
||||
commands.InfoDomain(name="test.gov"), cleaned=True
|
||||
)
|
||||
self.assertEqual(result, expected)
|
33
src/epplibwrapper/tests/utility/infoDomain.xml
Normal file
33
src/epplibwrapper/tests/utility/infoDomain.xml
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
|
||||
<epp xmlns:domain="urn:ietf:params:xml:ns:domain-1.0" xmlns:contact="urn:ietf:params:xml:ns:contact-1.0" xmlns:fee="urn:ietf:params:xml:ns:fee-0.6" xmlns:packageToken="urn:google:params:xml:ns:packageToken-1.0" xmlns="urn:ietf:params:xml:ns:epp-1.0" xmlns:fee11="urn:ietf:params:xml:ns:fee-0.11" xmlns:fee12="urn:ietf:params:xml:ns:fee-0.12" xmlns:launch="urn:ietf:params:xml:ns:launch-1.0" xmlns:secDNS="urn:ietf:params:xml:ns:secDNS-1.1" xmlns:host="urn:ietf:params:xml:ns:host-1.0">
|
||||
<response>
|
||||
<result code="1000">
|
||||
<msg>Command completed successfully</msg>
|
||||
</result>
|
||||
<resData>
|
||||
<domain:infData>
|
||||
<domain:name>test3.gov</domain:name>
|
||||
<domain:roid>DF1340360-GOV</domain:roid>
|
||||
<domain:status s="serverTransferProhibited"/>
|
||||
<domain:status s="inactive"/>
|
||||
<domain:registrant>TuaWnx9hnm84GCSU</domain:registrant>
|
||||
<domain:contact type="security">CONT2</domain:contact>
|
||||
<domain:contact type="tech">CONT3</domain:contact>
|
||||
<domain:clID>gov2023-ote</domain:clID>
|
||||
<domain:crID>gov2023-ote</domain:crID>
|
||||
<domain:crDate>2023-08-15T23:56:36Z</domain:crDate>
|
||||
<domain:upID>gov2023-ote</domain:upID>
|
||||
<domain:upDate>2023-08-17T02:03:19Z</domain:upDate>
|
||||
<domain:exDate>2024-08-15T23:56:36Z</domain:exDate>
|
||||
<domain:authInfo>
|
||||
<domain:pw>2fooBAR123fooBaz</domain:pw>
|
||||
</domain:authInfo>
|
||||
</domain:infData>
|
||||
</resData>
|
||||
<trID>
|
||||
<svTRID>wRRNVhKhQW2m6wsUHbo/lA==-29a</svTRID>
|
||||
</trID>
|
||||
</response>
|
||||
|
||||
</epp>
|
134
src/epplibwrapper/utility/pool.py
Normal file
134
src/epplibwrapper/utility/pool.py
Normal file
|
@ -0,0 +1,134 @@
|
|||
import logging
|
||||
from typing import List
|
||||
import gevent
|
||||
from geventconnpool import ConnectionPool
|
||||
from epplibwrapper.socket import Socket
|
||||
from epplibwrapper.utility.pool_error import PoolError, PoolErrorCodes
|
||||
|
||||
try:
|
||||
from epplib.commands import Hello
|
||||
from epplib.exceptions import TransportError
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from gevent.lock import BoundedSemaphore
|
||||
from collections import deque
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EPPConnectionPool(ConnectionPool):
|
||||
"""A connection pool for EPPLib.
|
||||
|
||||
Args:
|
||||
client (Client): The client
|
||||
login (commands.Login): Login creds
|
||||
options (dict): Options for the ConnectionPool
|
||||
base class
|
||||
"""
|
||||
|
||||
def __init__(self, client, login, options: dict):
|
||||
# For storing shared credentials
|
||||
self._client = client
|
||||
self._login = login
|
||||
|
||||
# Keep track of each greenlet
|
||||
self.greenlets: List[gevent.Greenlet] = []
|
||||
|
||||
# Define optional pool settings.
|
||||
# Kept in a dict so that the parent class,
|
||||
# client.py, can maintain seperation/expandability
|
||||
self.size = 1
|
||||
if "size" in options:
|
||||
self.size = options["size"]
|
||||
|
||||
self.exc_classes = tuple((TransportError,))
|
||||
if "exc_classes" in options:
|
||||
self.exc_classes = options["exc_classes"]
|
||||
|
||||
self.keepalive = None
|
||||
if "keepalive" in options:
|
||||
self.keepalive = options["keepalive"]
|
||||
|
||||
# Determines the period in which new
|
||||
# gevent threads are spun up.
|
||||
# This time period is in seconds. So for instance, .1 would be .1 seconds.
|
||||
self.spawn_frequency = 0.1
|
||||
if "spawn_frequency" in options:
|
||||
self.spawn_frequency = options["spawn_frequency"]
|
||||
|
||||
self.conn: deque = deque()
|
||||
self.lock = BoundedSemaphore(self.size)
|
||||
|
||||
self.populate_all_connections()
|
||||
|
||||
def _new_connection(self):
|
||||
socket = self._create_socket(self._client, self._login)
|
||||
try:
|
||||
connection = socket.connect()
|
||||
return connection
|
||||
except Exception as err:
|
||||
message = f"Failed to execute due to a registry error: {err}"
|
||||
logger.error(message, exc_info=True)
|
||||
# We want to raise a pool error rather than a LoginError here
|
||||
# because if this occurs internally, we should handle this
|
||||
# differently than we otherwise would for LoginError.
|
||||
raise PoolError(code=PoolErrorCodes.NEW_CONNECTION_FAILED) from err
|
||||
|
||||
def _keepalive(self, c):
|
||||
"""Sends a command to the server to keep the connection alive."""
|
||||
try:
|
||||
# Sends a ping to the registry via EPPLib
|
||||
c.send(Hello())
|
||||
except Exception as err:
|
||||
message = "Failed to keep the connection alive."
|
||||
logger.error(message, exc_info=True)
|
||||
raise PoolError(code=PoolErrorCodes.KEEP_ALIVE_FAILED) from err
|
||||
|
||||
def _create_socket(self, client, login) -> Socket:
|
||||
"""Creates and returns a socket instance"""
|
||||
socket = Socket(client, login)
|
||||
return socket
|
||||
|
||||
def get_connections(self):
|
||||
"""Returns the connection queue"""
|
||||
return self.conn
|
||||
|
||||
def kill_all_connections(self):
|
||||
"""Kills all active connections in the pool."""
|
||||
try:
|
||||
if len(self.conn) > 0:
|
||||
gevent.killall(self.greenlets)
|
||||
|
||||
self.greenlets.clear()
|
||||
self.conn.clear()
|
||||
|
||||
# Clear the semaphore
|
||||
self.lock = BoundedSemaphore(self.size)
|
||||
else:
|
||||
logger.info("No connections to kill.")
|
||||
except Exception as err:
|
||||
logger.error("Could not kill all connections.")
|
||||
raise PoolError(code=PoolErrorCodes.KILL_ALL_FAILED) from err
|
||||
|
||||
def populate_all_connections(self):
|
||||
"""Generates the connection pool.
|
||||
If any connections exist, kill them first.
|
||||
Based off of the __init__ definition for geventconnpool.
|
||||
"""
|
||||
if len(self.conn) > 0:
|
||||
self.kill_all_connections()
|
||||
|
||||
# Setup the lock
|
||||
for i in range(self.size):
|
||||
self.lock.acquire()
|
||||
|
||||
# Open multiple connections
|
||||
for i in range(self.size):
|
||||
self.greenlets.append(
|
||||
gevent.spawn_later(self.spawn_frequency * i, self._addOne)
|
||||
)
|
||||
|
||||
# Open a "keepalive" thread if we want to ping open connections
|
||||
if self.keepalive:
|
||||
self.greenlets.append(gevent.spawn(self._keepalive_periodic))
|
52
src/epplibwrapper/utility/pool_error.py
Normal file
52
src/epplibwrapper/utility/pool_error.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
from enum import IntEnum
|
||||
|
||||
|
||||
class PoolErrorCodes(IntEnum):
|
||||
"""Used in the PoolError class for
|
||||
error mapping.
|
||||
|
||||
Overview of contact error codes:
|
||||
- 2000 KILL_ALL_FAILED
|
||||
- 2001 NEW_CONNECTION_FAILED
|
||||
- 2002 KEEP_ALIVE_FAILED
|
||||
"""
|
||||
|
||||
KILL_ALL_FAILED = 2000
|
||||
NEW_CONNECTION_FAILED = 2001
|
||||
KEEP_ALIVE_FAILED = 2002
|
||||
|
||||
|
||||
class PoolError(Exception):
|
||||
"""
|
||||
Overview of contact error codes:
|
||||
- 2000 KILL_ALL_FAILED
|
||||
- 2001 NEW_CONNECTION_FAILED
|
||||
- 2002 KEEP_ALIVE_FAILED
|
||||
|
||||
Note: These are separate from the error codes returned from EppLib
|
||||
"""
|
||||
|
||||
# Used variables due to linter requirements
|
||||
kill_failed = "Could not kill all connections. Are multiple pools running?"
|
||||
conn_failed = (
|
||||
"Failed to execute due to a registry error."
|
||||
" See previous logs to determine the cause of the error."
|
||||
)
|
||||
alive_failed = (
|
||||
"Failed to keep the connection alive. "
|
||||
"It is likely that the registry returned a LoginError."
|
||||
)
|
||||
_error_mapping = {
|
||||
PoolErrorCodes.KILL_ALL_FAILED: kill_failed,
|
||||
PoolErrorCodes.NEW_CONNECTION_FAILED: conn_failed,
|
||||
PoolErrorCodes.KEEP_ALIVE_FAILED: alive_failed,
|
||||
}
|
||||
|
||||
def __init__(self, *args, code=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.code = code
|
||||
if self.code in self._error_mapping:
|
||||
self.message = self._error_mapping.get(self.code)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.message}"
|
12
src/epplibwrapper/utility/pool_status.py
Normal file
12
src/epplibwrapper/utility/pool_status.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
class PoolStatus:
|
||||
"""A list of Booleans to keep track of Pool Status.
|
||||
|
||||
pool_running -> bool: Tracks if the pool itself is active or not.
|
||||
connection_success -> bool: Tracks if connection is possible with the registry.
|
||||
pool_hanging -> pool: Tracks if the pool has exceeded its timeout period.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.pool_running = False
|
||||
self.connection_success = False
|
||||
self.pool_hanging = False
|
|
@ -219,9 +219,9 @@ class MyUserAdmin(BaseUserAdmin):
|
|||
# (which should in theory be the ONLY group)
|
||||
def group(self, obj):
|
||||
if obj.groups.filter(name="full_access_group").exists():
|
||||
return "Full access"
|
||||
return "full_access_group"
|
||||
elif obj.groups.filter(name="cisa_analysts_group").exists():
|
||||
return "Analyst"
|
||||
return "cisa_analysts_group"
|
||||
return ""
|
||||
|
||||
def get_list_display(self, request):
|
||||
|
@ -294,6 +294,26 @@ class ContactAdmin(ListHeaderAdmin):
|
|||
|
||||
contact.admin_order_field = "first_name" # type: ignore
|
||||
|
||||
# Read only that we'll leverage for CISA Analysts
|
||||
analyst_readonly_fields = [
|
||||
"user",
|
||||
]
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
"""Set the read-only state on form elements.
|
||||
We have 1 conditions that determine which fields are read-only:
|
||||
admin user permissions.
|
||||
"""
|
||||
|
||||
readonly_fields = list(self.readonly_fields)
|
||||
|
||||
if request.user.has_perm("registrar.full_access_permission"):
|
||||
return readonly_fields
|
||||
# Return restrictive Read-only fields for analysts and
|
||||
# users who might not belong to groups
|
||||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||
return readonly_fields # Read-only fields for analysts
|
||||
|
||||
|
||||
class WebsiteAdmin(ListHeaderAdmin):
|
||||
"""Custom website admin class."""
|
||||
|
@ -420,9 +440,6 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
|||
"creator",
|
||||
"type_of_work",
|
||||
"more_organization_information",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"zipcode",
|
||||
"domain",
|
||||
"submitter",
|
||||
"no_other_contacts_rationale",
|
||||
|
@ -557,9 +574,6 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
|||
analyst_readonly_fields = [
|
||||
"creator",
|
||||
"about_your_organization",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"zipcode",
|
||||
"requested_domain",
|
||||
"alternative_domains",
|
||||
"purpose",
|
||||
|
@ -721,7 +735,7 @@ class DomainAdmin(ListHeaderAdmin):
|
|||
]
|
||||
|
||||
def organization_type(self, obj):
|
||||
return obj.domain_info.organization_type
|
||||
return obj.domain_info.get_organization_type_display()
|
||||
|
||||
organization_type.admin_order_field = ( # type: ignore
|
||||
"domain_info__organization_type"
|
||||
|
|
|
@ -383,3 +383,30 @@ function prepareDeleteButtons() {
|
|||
}
|
||||
|
||||
})();
|
||||
|
||||
/**
|
||||
* An IIFE that triggers a modal on the DS Data Form under certain conditions
|
||||
*
|
||||
*/
|
||||
(function triggerModalOnDsDataForm() {
|
||||
let saveButon = document.querySelector("#save-ds-data");
|
||||
|
||||
// The view context will cause a hitherto hidden modal trigger to
|
||||
// show up. On save, we'll test for that modal trigger appearing. We'll
|
||||
// run that test once every 100 ms for 5 secs, which should balance performance
|
||||
// while accounting for network or lag issues.
|
||||
if (saveButon) {
|
||||
let i = 0;
|
||||
var tryToTriggerModal = setInterval(function() {
|
||||
i++;
|
||||
if (i > 100) {
|
||||
clearInterval(tryToTriggerModal);
|
||||
}
|
||||
let modalTrigger = document.querySelector("#ds-toggle-dnssec-alert");
|
||||
if (modalTrigger) {
|
||||
modalTrigger.click()
|
||||
clearInterval(tryToTriggerModal);
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -22,8 +22,11 @@ a.breadcrumb__back {
|
|||
}
|
||||
}
|
||||
|
||||
a.usa-button:not(.usa-button--unstyled, .usa-button--outline) {
|
||||
a.usa-button {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.usa-button:not(.usa-button--unstyled, .usa-button--outline) {
|
||||
color: color('white');
|
||||
}
|
||||
|
||||
|
@ -111,15 +114,3 @@ a.usa-button--unstyled:visited {
|
|||
margin-left: units(2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// WARNING: crazy hack ahead:
|
||||
// Cancel button(s) on the DNSSEC form pages
|
||||
// We want to position the cancel button on the
|
||||
// dnssec forms next to the submit button
|
||||
// This button's markup is in its own form
|
||||
.btn-cancel {
|
||||
position: relative;
|
||||
top: -39.2px;
|
||||
left: 88px;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
margin-top: units(3);
|
||||
}
|
||||
|
||||
.usa-form .usa-button.margin-top-1 {
|
||||
margin-top: units(1);
|
||||
}
|
||||
|
||||
.usa-form--extra-large {
|
||||
max-width: none;
|
||||
}
|
||||
|
|
|
@ -534,6 +534,20 @@ SECRET_REGISTRY_KEY = secret_registry_key
|
|||
SECRET_REGISTRY_KEY_PASSPHRASE = secret_registry_key_passphrase
|
||||
SECRET_REGISTRY_HOSTNAME = secret_registry_hostname
|
||||
|
||||
# Use this variable to set the size of our connection pool in client.py
|
||||
# WARNING: Setting this value too high could cause frequent app crashes!
|
||||
# Having too many connections open could cause the sandbox to timeout,
|
||||
# as the spinup time could exceed the timeout time.
|
||||
EPP_CONNECTION_POOL_SIZE = 1
|
||||
|
||||
# Determines the interval in which we ping open connections in seconds
|
||||
# Calculated as POOL_KEEP_ALIVE / EPP_CONNECTION_POOL_SIZE
|
||||
POOL_KEEP_ALIVE = 60
|
||||
|
||||
# Determines how long we try to keep a pool alive for,
|
||||
# before restarting it.
|
||||
POOL_TIMEOUT = 60
|
||||
|
||||
# endregion
|
||||
# region: Security and Privacy----------------------------------------------###
|
||||
|
||||
|
@ -581,7 +595,7 @@ ALLOWED_HOSTS = [
|
|||
"getgov-bl.app.cloud.gov",
|
||||
"getgov-rjm.app.cloud.gov",
|
||||
"getgov-dk.app.cloud.gov",
|
||||
"get.gov",
|
||||
"manage.get.gov",
|
||||
]
|
||||
|
||||
# Extend ALLOWED_HOSTS.
|
||||
|
|
|
@ -100,11 +100,6 @@ urlpatterns = [
|
|||
views.DomainDsDataView.as_view(),
|
||||
name="domain-dns-dnssec-dsdata",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/dns/dnssec/keydata",
|
||||
views.DomainKeyDataView.as_view(),
|
||||
name="domain-dns-dnssec-keydata",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/your-contact-information",
|
||||
views.DomainYourContactInformationView.as_view(),
|
||||
|
|
|
@ -86,6 +86,12 @@ class UserFixture:
|
|||
"first_name": "Kristina",
|
||||
"last_name": "Yin",
|
||||
},
|
||||
{
|
||||
"username": "ac49d7c1-368a-4e6b-8f1d-60250e20a16f",
|
||||
"first_name": "Vicky",
|
||||
"last_name": "Chin",
|
||||
"email": "szu.chin@associates.cisa.dhs.gov",
|
||||
},
|
||||
]
|
||||
|
||||
STAFF = [
|
||||
|
@ -150,6 +156,12 @@ class UserFixture:
|
|||
"last_name": "Yin-Analyst",
|
||||
"email": "kristina.yin+1@gsa.gov",
|
||||
},
|
||||
{
|
||||
"username": "8f42302e-b83a-4c9e-8764-fc19e2cea576",
|
||||
"first_name": "Vickster-Analyst",
|
||||
"last_name": "Chin-Analyst",
|
||||
"email": "szu.chin@ecstech.com",
|
||||
},
|
||||
]
|
||||
|
||||
def load_users(cls, users, group_name):
|
||||
|
|
|
@ -8,6 +8,4 @@ from .domain import (
|
|||
DomainDnssecForm,
|
||||
DomainDsdataFormset,
|
||||
DomainDsdataForm,
|
||||
DomainKeydataFormset,
|
||||
DomainKeydataForm,
|
||||
)
|
||||
|
|
|
@ -153,7 +153,8 @@ class RegistrarFormSet(forms.BaseFormSet):
|
|||
|
||||
class OrganizationTypeForm(RegistrarForm):
|
||||
organization_type = forms.ChoiceField(
|
||||
choices=DomainApplication.OrganizationChoices.choices,
|
||||
# use the long names in the application form
|
||||
choices=DomainApplication.OrganizationChoicesVerbose.choices,
|
||||
widget=forms.RadioSelect,
|
||||
error_messages={"required": "Select the type of organization you represent."},
|
||||
)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# common.py
|
||||
#
|
||||
# ALGORITHM_CHOICES are options for alg attribute in DS Data and Key Data
|
||||
# ALGORITHM_CHOICES are options for alg attribute in DS Data
|
||||
# reference:
|
||||
# https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml
|
||||
ALGORITHM_CHOICES = [
|
||||
|
@ -24,15 +24,3 @@ DIGEST_TYPE_CHOICES = [
|
|||
(0, "(0) Reserved"),
|
||||
(1, "(1) SHA-256"),
|
||||
]
|
||||
# PROTOCOL_CHOICES are options for protocol attribute in Key Data
|
||||
# reference: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.2
|
||||
PROTOCOL_CHOICES = [
|
||||
(3, "(3) DNSSEC"),
|
||||
]
|
||||
# FLAG_CHOICES are options for flags attribute in Key Data
|
||||
# reference: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.1
|
||||
FLAG_CHOICES = [
|
||||
(0, "(0)"),
|
||||
(256, "(256) ZSK"),
|
||||
(257, "(257) KSK"),
|
||||
]
|
||||
|
|
|
@ -10,8 +10,6 @@ from ..models import Contact, DomainInformation
|
|||
from .common import (
|
||||
ALGORITHM_CHOICES,
|
||||
DIGEST_TYPE_CHOICES,
|
||||
FLAG_CHOICES,
|
||||
PROTOCOL_CHOICES,
|
||||
)
|
||||
|
||||
|
||||
|
@ -188,44 +186,3 @@ DomainDsdataFormset = formset_factory(
|
|||
extra=0,
|
||||
can_delete=True,
|
||||
)
|
||||
|
||||
|
||||
class DomainKeydataForm(forms.Form):
|
||||
"""Form for adding or editing DNSSEC Key Data to a domain."""
|
||||
|
||||
flag = forms.TypedChoiceField(
|
||||
required=True,
|
||||
label="Flag",
|
||||
coerce=int,
|
||||
choices=FLAG_CHOICES,
|
||||
error_messages={"required": ("Flag is required.")},
|
||||
)
|
||||
|
||||
protocol = forms.TypedChoiceField(
|
||||
required=True,
|
||||
label="Protocol",
|
||||
coerce=int,
|
||||
choices=PROTOCOL_CHOICES,
|
||||
error_messages={"required": ("Protocol is required.")},
|
||||
)
|
||||
|
||||
algorithm = forms.TypedChoiceField(
|
||||
required=True,
|
||||
label="Algorithm",
|
||||
coerce=int,
|
||||
choices=[(None, "--Select--")] + ALGORITHM_CHOICES, # type: ignore
|
||||
error_messages={"required": ("Algorithm is required.")},
|
||||
)
|
||||
|
||||
pub_key = forms.CharField(
|
||||
required=True,
|
||||
label="Pub key",
|
||||
error_messages={"required": ("Pub key is required.")},
|
||||
)
|
||||
|
||||
|
||||
DomainKeydataFormset = formset_factory(
|
||||
DomainKeydataForm,
|
||||
extra=0,
|
||||
can_delete=True,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
# Generated by Django 4.2.1 on 2023-10-20 21:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0040_alter_userdomainrole_role"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="organization_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("federal", "Federal"),
|
||||
("interstate", "Interstate"),
|
||||
("state_or_territory", "State or territory"),
|
||||
("tribal", "Tribal"),
|
||||
("county", "County"),
|
||||
("city", "City"),
|
||||
("special_district", "Special district"),
|
||||
("school_district", "School district"),
|
||||
],
|
||||
help_text="Type of organization",
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="organization_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("federal", "Federal"),
|
||||
("interstate", "Interstate"),
|
||||
("state_or_territory", "State or territory"),
|
||||
("tribal", "Tribal"),
|
||||
("county", "County"),
|
||||
("city", "City"),
|
||||
("special_district", "Special district"),
|
||||
("school_district", "School district"),
|
||||
],
|
||||
help_text="Type of Organization",
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
37
src/registrar/migrations/0042_create_groups_v03.py
Normal file
37
src/registrar/migrations/0042_create_groups_v03.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
|
||||
# It is dependent on 0035 (which populates ContentType and Permissions)
|
||||
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
|
||||
# in the user_group model then:
|
||||
# [NOT RECOMMENDED]
|
||||
# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions
|
||||
# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups
|
||||
# step 3: fake run the latest migration in the migrations list
|
||||
# [RECOMMENDED]
|
||||
# Alternatively:
|
||||
# step 1: duplicate the migration that loads data
|
||||
# step 2: docker-compose exec app ./manage.py migrate
|
||||
|
||||
from django.db import migrations
|
||||
from registrar.models import UserGroup
|
||||
from typing import Any
|
||||
|
||||
|
||||
# For linting: RunPython expects a function reference,
|
||||
# so let's give it one
|
||||
def create_groups(apps, schema_editor) -> Any:
|
||||
UserGroup.create_cisa_analyst_group(apps, schema_editor)
|
||||
UserGroup.create_full_access_group(apps, schema_editor)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0041_alter_domainapplication_organization_type_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
create_groups,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
atomic=True,
|
||||
),
|
||||
]
|
|
@ -1,5 +1,4 @@
|
|||
from auditlog.registry import auditlog # type: ignore
|
||||
|
||||
from .contact import Contact
|
||||
from .domain_application import DomainApplication
|
||||
from .domain_information import DomainInformation
|
||||
|
|
|
@ -9,6 +9,14 @@ from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignor
|
|||
|
||||
from django.db import models
|
||||
from typing import Any
|
||||
|
||||
|
||||
from registrar.utility.errors import (
|
||||
ActionNotAllowed,
|
||||
NameserverError,
|
||||
NameserverErrorCodes as nsErrorCodes,
|
||||
)
|
||||
|
||||
from epplibwrapper import (
|
||||
CLIENT as registry,
|
||||
commands,
|
||||
|
@ -19,15 +27,8 @@ from epplibwrapper import (
|
|||
ErrorCode,
|
||||
)
|
||||
|
||||
from registrar.utility.errors import (
|
||||
ActionNotAllowed,
|
||||
NameserverError,
|
||||
NameserverErrorCodes as nsErrorCodes,
|
||||
)
|
||||
|
||||
from registrar.models.utility.contact_error import ContactError, ContactErrorCodes
|
||||
|
||||
|
||||
from .utility.domain_field import DomainField
|
||||
from .utility.domain_helper import DomainHelper
|
||||
from .utility.time_stamped_model import TimeStampedModel
|
||||
|
@ -522,12 +523,11 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
addExtension: dict
|
||||
remExtension: dict
|
||||
|
||||
addExtension includes all dsData or keyData to be added
|
||||
remExtension includes all dsData or keyData to be removed
|
||||
addExtension includes all dsData to be added
|
||||
remExtension includes all dsData to be removed
|
||||
|
||||
method operates on dsData OR keyData, never a mix of the two;
|
||||
operates based on which is present in _dnssecdata;
|
||||
if neither is present, addExtension will be empty dict, and
|
||||
method operates on dsData;
|
||||
if dsData is not present, addExtension will be empty dict, and
|
||||
remExtension will be all existing dnssecdata to be deleted
|
||||
"""
|
||||
|
||||
|
@ -560,32 +560,9 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
else:
|
||||
addDnssecdata["dsData"] = None
|
||||
|
||||
elif _dnssecdata and _dnssecdata.keyData is not None:
|
||||
# initialize addDnssecdata and remDnssecdata for keyData
|
||||
addDnssecdata["keyData"] = _dnssecdata.keyData
|
||||
|
||||
if oldDnssecdata and len(oldDnssecdata.keyData) > 0:
|
||||
# if existing keyData not in new keyData, mark for removal
|
||||
keyDataForRemoval = [
|
||||
keyData
|
||||
for keyData in oldDnssecdata.keyData
|
||||
if keyData not in _dnssecdata.keyData
|
||||
]
|
||||
if len(keyDataForRemoval) > 0:
|
||||
remDnssecdata["keyData"] = keyDataForRemoval
|
||||
|
||||
# if new keyData not in existing keyData, mark for add
|
||||
keyDataForAdd = [
|
||||
keyData
|
||||
for keyData in _dnssecdata.keyData
|
||||
if keyData not in oldDnssecdata.keyData
|
||||
]
|
||||
if len(keyDataForAdd) > 0:
|
||||
addDnssecdata["keyData"] = keyDataForAdd
|
||||
else:
|
||||
# there are no new dsData or keyData, remove all
|
||||
# there are no new dsData, remove all dsData from existing
|
||||
remDnssecdata["dsData"] = getattr(oldDnssecdata, "dsData", None)
|
||||
remDnssecdata["keyData"] = getattr(oldDnssecdata, "keyData", None)
|
||||
|
||||
return addDnssecdata, remDnssecdata
|
||||
|
||||
|
@ -595,12 +572,10 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
addParams = {
|
||||
"maxSigLife": _addDnssecdata.get("maxSigLife", None),
|
||||
"dsData": _addDnssecdata.get("dsData", None),
|
||||
"keyData": _addDnssecdata.get("keyData", None),
|
||||
}
|
||||
remParams = {
|
||||
"maxSigLife": _remDnssecdata.get("maxSigLife", None),
|
||||
"remDsData": _remDnssecdata.get("dsData", None),
|
||||
"remKeyData": _remDnssecdata.get("keyData", None),
|
||||
}
|
||||
addRequest = commands.UpdateDomain(name=self.name)
|
||||
addExtension = commands.UpdateDomainDNSSECExtension(**addParams)
|
||||
|
@ -609,19 +584,9 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
remExtension = commands.UpdateDomainDNSSECExtension(**remParams)
|
||||
remRequest.add_extension(remExtension)
|
||||
try:
|
||||
if (
|
||||
"dsData" in _addDnssecdata
|
||||
and _addDnssecdata["dsData"] is not None
|
||||
or "keyData" in _addDnssecdata
|
||||
and _addDnssecdata["keyData"] is not None
|
||||
):
|
||||
if "dsData" in _addDnssecdata and _addDnssecdata["dsData"] is not None:
|
||||
registry.send(addRequest, cleaned=True)
|
||||
if (
|
||||
"dsData" in _remDnssecdata
|
||||
and _remDnssecdata["dsData"] is not None
|
||||
or "keyData" in _remDnssecdata
|
||||
and _remDnssecdata["keyData"] is not None
|
||||
):
|
||||
if "dsData" in _remDnssecdata and _remDnssecdata["dsData"] is not None:
|
||||
registry.send(remRequest, cleaned=True)
|
||||
except RegistryError as e:
|
||||
logger.error(
|
||||
|
|
|
@ -105,28 +105,57 @@ class DomainApplication(TimeStampedModel):
|
|||
ARMED_FORCES_AP = "AP", "Armed Forces Pacific (AP)"
|
||||
|
||||
class OrganizationChoices(models.TextChoices):
|
||||
|
||||
"""
|
||||
Primary organization choices:
|
||||
For use in django admin
|
||||
Keys need to match OrganizationChoicesVerbose
|
||||
"""
|
||||
|
||||
FEDERAL = "federal", "Federal"
|
||||
INTERSTATE = "interstate", "Interstate"
|
||||
STATE_OR_TERRITORY = "state_or_territory", "State or territory"
|
||||
TRIBAL = "tribal", "Tribal"
|
||||
COUNTY = "county", "County"
|
||||
CITY = "city", "City"
|
||||
SPECIAL_DISTRICT = "special_district", "Special district"
|
||||
SCHOOL_DISTRICT = "school_district", "School district"
|
||||
|
||||
class OrganizationChoicesVerbose(models.TextChoices):
|
||||
|
||||
"""
|
||||
Secondary organization choices
|
||||
For use in the application form and on the templates
|
||||
Keys need to match OrganizationChoices
|
||||
"""
|
||||
|
||||
FEDERAL = (
|
||||
"federal",
|
||||
"Federal: an agency of the U.S. government's executive, legislative, "
|
||||
"or judicial branches",
|
||||
"Federal: an agency of the U.S. government's executive, "
|
||||
"legislative, or judicial branches",
|
||||
)
|
||||
INTERSTATE = "interstate", "Interstate: an organization of two or more states"
|
||||
STATE_OR_TERRITORY = "state_or_territory", (
|
||||
"State or territory: one of the 50 U.S. states, the District of "
|
||||
"Columbia, American Samoa, Guam, Northern Mariana Islands, "
|
||||
"Puerto Rico, or the U.S. Virgin Islands"
|
||||
STATE_OR_TERRITORY = (
|
||||
"state_or_territory",
|
||||
"State or territory: one of the 50 U.S. states, the District of Columbia, "
|
||||
"American Samoa, Guam, Northern Mariana Islands, Puerto Rico, or the U.S. "
|
||||
"Virgin Islands",
|
||||
)
|
||||
TRIBAL = "tribal", (
|
||||
"Tribal: a tribal government recognized by the federal or "
|
||||
"a state government"
|
||||
TRIBAL = (
|
||||
"tribal",
|
||||
"Tribal: a tribal government recognized by the federal or a state "
|
||||
"government",
|
||||
)
|
||||
COUNTY = "county", "County: a county, parish, or borough"
|
||||
CITY = "city", "City: a city, town, township, village, etc."
|
||||
SPECIAL_DISTRICT = "special_district", (
|
||||
"Special district: an independent organization within a single state"
|
||||
SPECIAL_DISTRICT = (
|
||||
"special_district",
|
||||
"Special district: an independent organization within a single state",
|
||||
)
|
||||
SCHOOL_DISTRICT = "school_district", (
|
||||
"School district: a school district that is not part of a local government"
|
||||
SCHOOL_DISTRICT = (
|
||||
"school_district",
|
||||
"School district: a school district that is not part of a local "
|
||||
"government",
|
||||
)
|
||||
|
||||
class BranchChoices(models.TextChoices):
|
||||
|
@ -297,6 +326,7 @@ class DomainApplication(TimeStampedModel):
|
|||
# ##### data fields from the initial form #####
|
||||
organization_type = models.CharField(
|
||||
max_length=255,
|
||||
# use the short names in Django admin
|
||||
choices=OrganizationChoices.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
|
|
|
@ -21,6 +21,7 @@ class DomainInformation(TimeStampedModel):
|
|||
|
||||
StateTerritoryChoices = DomainApplication.StateTerritoryChoices
|
||||
|
||||
# use the short names in Django admin
|
||||
OrganizationChoices = DomainApplication.OrganizationChoices
|
||||
|
||||
BranchChoices = DomainApplication.BranchChoices
|
||||
|
|
|
@ -4,6 +4,9 @@ from django.contrib.auth.models import AbstractUser
|
|||
from django.db import models
|
||||
|
||||
from .domain_invitation import DomainInvitation
|
||||
from .transition_domain import TransitionDomain
|
||||
from .domain_information import DomainInformation
|
||||
from .domain import Domain
|
||||
|
||||
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
|
||||
|
||||
|
@ -62,12 +65,9 @@ class User(AbstractUser):
|
|||
def is_restricted(self):
|
||||
return self.status == self.RESTRICTED
|
||||
|
||||
def first_login(self):
|
||||
"""Callback when the user is authenticated for the very first time.
|
||||
|
||||
When a user first arrives on the site, we need to retrieve any domain
|
||||
invitations that match their email address.
|
||||
"""
|
||||
def check_domain_invitations_on_login(self):
|
||||
"""When a user first arrives on the site, we need to retrieve any domain
|
||||
invitations that match their email address."""
|
||||
for invitation in DomainInvitation.objects.filter(
|
||||
email=self.email, status=DomainInvitation.INVITED
|
||||
):
|
||||
|
@ -82,6 +82,101 @@ class User(AbstractUser):
|
|||
"Failed to retrieve invitation %s", invitation, exc_info=True
|
||||
)
|
||||
|
||||
def create_domain_and_invite(self, transition_domain: TransitionDomain):
|
||||
transition_domain_name = transition_domain.domain_name
|
||||
transition_domain_status = transition_domain.status
|
||||
transition_domain_email = transition_domain.username
|
||||
|
||||
# type safety check. name should never be none
|
||||
if transition_domain_name is not None:
|
||||
new_domain = Domain(
|
||||
name=transition_domain_name, state=transition_domain_status
|
||||
)
|
||||
new_domain.save()
|
||||
# check that a domain invitation doesn't already
|
||||
# exist for this e-mail / Domain pair
|
||||
domain_email_already_in_domain_invites = DomainInvitation.objects.filter(
|
||||
email=transition_domain_email.lower(), domain=new_domain
|
||||
).exists()
|
||||
if not domain_email_already_in_domain_invites:
|
||||
# Create new domain invitation
|
||||
new_domain_invitation = DomainInvitation(
|
||||
email=transition_domain_email.lower(), domain=new_domain
|
||||
)
|
||||
new_domain_invitation.save()
|
||||
|
||||
def check_transition_domains_on_login(self):
|
||||
"""When a user first arrives on the site, we need to check
|
||||
if they are logging in with the same e-mail as a
|
||||
transition domain and update our database accordingly."""
|
||||
|
||||
for transition_domain in TransitionDomain.objects.filter(username=self.email):
|
||||
# Looks like the user logged in with the same e-mail as
|
||||
# one or more corresponding transition domains.
|
||||
# Create corresponding DomainInformation objects.
|
||||
|
||||
# NOTE: adding an ADMIN user role for this user
|
||||
# for each domain should already be done
|
||||
# in the invitation.retrieve() method.
|
||||
# However, if the migration scripts for transition
|
||||
# domain objects were not executed correctly,
|
||||
# there could be transition domains without
|
||||
# any corresponding Domain & DomainInvitation objects,
|
||||
# which means the invitation.retrieve() method might
|
||||
# not execute.
|
||||
# Check that there is a corresponding domain object
|
||||
# for this transition domain. If not, we have an error
|
||||
# with our data and migrations need to be run again.
|
||||
|
||||
# Get the domain that corresponds with this transition domain
|
||||
domain_exists = Domain.objects.filter(
|
||||
name=transition_domain.domain_name
|
||||
).exists()
|
||||
if not domain_exists:
|
||||
logger.warn(
|
||||
"""There are transition domains without
|
||||
corresponding domain objects!
|
||||
Please run migration scripts for transition domains
|
||||
(See data_migration.md)"""
|
||||
)
|
||||
# No need to throw an exception...just create a domain
|
||||
# and domain invite, then proceed as normal
|
||||
self.create_domain_and_invite(transition_domain)
|
||||
|
||||
domain = Domain.objects.get(name=transition_domain.domain_name)
|
||||
|
||||
# Create a domain information object, if one doesn't
|
||||
# already exist
|
||||
domain_info_exists = DomainInformation.objects.filter(
|
||||
domain=domain
|
||||
).exists()
|
||||
if not domain_info_exists:
|
||||
new_domain_info = DomainInformation(creator=self, domain=domain)
|
||||
new_domain_info.save()
|
||||
|
||||
def first_login(self):
|
||||
"""Callback when the user is authenticated for the very first time.
|
||||
|
||||
When a user first arrives on the site, we need to retrieve any domain
|
||||
invitations that match their email address.
|
||||
|
||||
We also need to check if they are logging in with the same e-mail
|
||||
as a transition domain and update our domainInfo objects accordingly.
|
||||
"""
|
||||
|
||||
# PART 1: TRANSITION DOMAINS
|
||||
#
|
||||
# NOTE: THIS MUST RUN FIRST
|
||||
# (If we have an issue where transition domains were
|
||||
# not fully converted into Domain and DomainInvitation
|
||||
# objects, this method will fill in the gaps.
|
||||
# This will ensure the Domain Invitations method
|
||||
# runs correctly (no missing invites))
|
||||
self.check_transition_domains_on_login()
|
||||
|
||||
# PART 2: DOMAIN INVITATIONS
|
||||
self.check_domain_invitations_on_login()
|
||||
|
||||
class Meta:
|
||||
permissions = [
|
||||
("analyst_access_permission", "Analyst Access Permission"),
|
||||
|
|
|
@ -24,7 +24,7 @@ class UserGroup(Group):
|
|||
{
|
||||
"app_label": "registrar",
|
||||
"model": "contact",
|
||||
"permissions": ["view_contact"],
|
||||
"permissions": ["change_contact"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
|
@ -56,6 +56,11 @@ class UserGroup(Group):
|
|||
"model": "domaininvitation",
|
||||
"permissions": ["add_domaininvitation", "view_domaininvitation"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "website",
|
||||
"permissions": ["change_website"],
|
||||
},
|
||||
]
|
||||
|
||||
# Avoid error: You can't execute queries until the end
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{% extends 'application_form.html' %}
|
||||
{% load static url_helpers %}
|
||||
{% load custom_filters %}
|
||||
|
||||
{% block form_required_fields_help_text %}
|
||||
{# there are no required fields on this page so don't show this #}
|
||||
|
@ -26,7 +27,13 @@
|
|||
<div class="review__step__name">{{ form_titles|get_item:step }}</div>
|
||||
<div>
|
||||
{% if step == Step.ORGANIZATION_TYPE %}
|
||||
{{ application.get_organization_type_display|default:"Incomplete" }}
|
||||
{% if application.organization_type is not None %}
|
||||
{% with long_org_type=application.organization_type|get_organization_long_name %}
|
||||
{{ long_org_type }}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
Incomplete
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if step == Step.TRIBAL_GOVERNMENT %}
|
||||
{{ application.tribe_name|default:"Incomplete" }}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% load custom_filters %}
|
||||
|
||||
{% block title %}Domain request status | {{ domainapplication.requested_domain.name }} | {% endblock %}
|
||||
{% load static url_helpers %}
|
||||
|
||||
|
@ -50,7 +52,9 @@
|
|||
<div class="grid-col desktop:grid-offset-2 maxw-tablet">
|
||||
<h2 class="text-primary-darker"> Summary of your domain request </h2>
|
||||
{% with heading_level='h3' %}
|
||||
{% include "includes/summary_item.html" with title='Type of organization' value=domainapplication.get_organization_type_display heading_level=heading_level %}
|
||||
{% with long_org_type=domainapplication.organization_type|get_organization_long_name %}
|
||||
{% include "includes/summary_item.html" with title='Type of organization' value=long_org_type heading_level=heading_level %}
|
||||
{% endwith %}
|
||||
|
||||
{% if domainapplication.tribe_name %}
|
||||
{% include "includes/summary_item.html" with title='Tribal government' value=domainapplication.tribe_name heading_level=heading_level %}
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
{% include "includes/summary_item.html" with title='Security email' value='None provided' edit_link=url %}
|
||||
{% endif %}
|
||||
{% url 'domain-users' pk=domain.id as url %}
|
||||
{% include "includes/summary_item.html" with title='User management' users='true' list=True value=domain.permissions.all edit_link=url %}
|
||||
{% include "includes/summary_item.html" with title='Domain managers' users='true' list=True value=domain.permissions.all edit_link=url %}
|
||||
|
||||
</div>
|
||||
{% endblock %} {# domain_content #}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
{% url 'domain-dns-nameservers' pk=domain.id as url %}
|
||||
<p><a href="{{ url }}">DNS name servers</a></p>
|
||||
|
||||
{% url 'domain-dnssec' pk=domain.id as url %}
|
||||
{% url 'domain-dns-dnssec' pk=domain.id as url %}
|
||||
<p><a href="{{ url }}">DNSSEC</a></p>
|
||||
|
||||
{% endblock %} {# domain_content #}
|
||||
|
|
|
@ -7,51 +7,45 @@
|
|||
|
||||
<h1>DNSSEC</h1>
|
||||
|
||||
<p>DNSSEC, or DNS Security Extensions, is additional security layer to protect your domain. Enabling DNSSEC ensures that when someone visits your domain, they can be certain that it's connecting to the correct server, preventing potential hijacking or tampering with your domain's records.</p>
|
||||
<p>DNSSEC, or DNS Security Extensions, is additional security layer to protect your website. Enabling DNSSEC ensures that when someone visits your website, they can be certain that it’s connecting to the correct server, preventing potential hijacking or tampering with your domain's records.</p>
|
||||
|
||||
<form class="usa-form usa-form--text-width" method="post">
|
||||
{% csrf_token %}
|
||||
{% if has_dnssec_records %}
|
||||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||
<div class="usa-alert__body">
|
||||
In order to fully disable DNSSEC on your domain, you will need to work with your DNS provider to remove your DNSSEC-related records from your zone.
|
||||
<div
|
||||
class="usa-summary-box dotgov-status-box padding-top-0"
|
||||
role="region"
|
||||
aria-labelledby="Important notes on disabling DNSSEC"
|
||||
>
|
||||
<div class="usa-summary-box__body">
|
||||
<p class="usa-summary-box__heading font-sans-md margin-bottom-0"
|
||||
id="summary-box-key-information"
|
||||
>
|
||||
<h2>To fully disable DNSSEC </h2>
|
||||
<ul class="usa-list">
|
||||
<li>Click “Disable DNSSEC” below.</li>
|
||||
<li>Wait until the Time to Live (TTL) expires on your DNSSEC records managed by your DNS hosting provider. This is often less than 24 hours, but confirm with your provider.</li>
|
||||
<li>After the TTL expiration, disable DNSSEC at your DNS hosting provider. </li>
|
||||
</ul>
|
||||
<p><strong>Warning:</strong> If you disable DNSSEC at your DNS hosting provider before TTL expiration, this may cause your domain to appear offline.</p>
|
||||
</div>
|
||||
</div>
|
||||
<h2>DNSSEC is enabled on your domain</h2>
|
||||
<a
|
||||
href="#toggle-dnssec-alert"
|
||||
class="usa-button"
|
||||
class="usa-button usa-button--outline margin-top-1"
|
||||
aria-controls="toggle-dnssec-alert"
|
||||
data-open-modal
|
||||
>Disable DNSSEC</a
|
||||
>
|
||||
{% elif dnssec_enabled %}
|
||||
<div id="add-records">
|
||||
<h2> Add DS Records </h2>
|
||||
<p>In order to enable DNSSEC and add Delegation Signer (DS) records, you must first configure it with your DNS hosting service. Your configuration will determine whether you need to add DS Data or Key Data. Contact your DNS hosting provider if you are unsure which record type to add.</p>
|
||||
<p>
|
||||
<a href="{% url 'domain-dns-dnssec-dsdata' pk=domain.id %}" class="usa-button usa-button--outline">Add DS Data</a>
|
||||
<a href="{% url 'domain-dns-dnssec-keydata' pk=domain.id %}" class="usa-button usa-button--outline">Add Key Data</a>
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button usa-button--unstyled usa-button--cancel"
|
||||
name="cancel_dnssec"
|
||||
id="cancel_dnssec"
|
||||
>Cancel</button>
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div id="enable-dnssec">
|
||||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||
<div class="usa-alert__body">
|
||||
It is strongly recommended that you only enable DNSSEC if you know how to set it up properly at your hosting service. If you make a mistake, it could cause your domain name to stop working.
|
||||
It is strongly recommended that you only enable DNSSEC if you know how to set it up properly at your hosting service. If you make a mistake, it could cause your domain name to stop working.
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
name="enable_dnssec"
|
||||
id="enable_dnssec"
|
||||
>Enable DNSSEC</button>
|
||||
<a href="{% url 'domain-dns-dnssec-dsdata' pk=domain.id %}" class="usa-button">Enable DNSSEC</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
@ -62,7 +56,7 @@
|
|||
aria-labelledby="Are you sure you want to continue?"
|
||||
aria-describedby="Your DNSSEC records will be deleted from the registry."
|
||||
>
|
||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to continue?" modal_description="Your DNSSEC records will be deleted from the registry." modal_button=modal_button|safe %}
|
||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to disable DNSSEC?" modal_button=modal_button|safe %}
|
||||
</div>
|
||||
|
||||
{% endblock %} {# domain_content #}
|
||||
|
|
|
@ -4,47 +4,24 @@
|
|||
{% block title %}DS Data | {{ domain.name }} | {% endblock %}
|
||||
|
||||
{% block domain_content %}
|
||||
{% for form in formset %}
|
||||
{% include "includes/form_errors.html" with form=form %}
|
||||
{% endfor %}
|
||||
|
||||
{% if domain.dnssecdata is None and not dnssec_ds_confirmed %}
|
||||
{% if domain.dnssecdata is None %}
|
||||
<div class="usa-alert usa-alert--info usa-alert--slim margin-bottom-3">
|
||||
<div class="usa-alert__body">
|
||||
You have no DS Data added. Enable DNSSEC by adding DS Data or return to the DNSSEC page and click 'enable.'
|
||||
You have no DS Data added. Enable DNSSEC by adding DS Data.
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for form in formset %}
|
||||
{% include "includes/form_errors.html" with form=form %}
|
||||
{% endfor %}
|
||||
|
||||
<h1>DS Data</h1>
|
||||
|
||||
{% if domain.dnssecdata is not None and domain.dnssecdata.keyData is not None %}
|
||||
<div class="usa-alert usa-alert--warning usa-alert--slim margin-bottom-3">
|
||||
<div class="usa-alert__body">
|
||||
<h4 class="usa-alert__heading">Warning, you cannot add DS Data</h4>
|
||||
<p class="usa-alert__text">
|
||||
You cannot add DS Data because you have already added Key Data. Delete your Key Data records in order to add DS Data.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% elif not dnssec_ds_confirmed %}
|
||||
<p>In order to enable DNSSEC, you must first configure it with your DNS hosting service.</p>
|
||||
<p>Enter the values given by your DNS provider for DS Data.</p>
|
||||
<p>Required fields are marked with an asterisk (<abbr
|
||||
title="required"
|
||||
class="usa-hint usa-hint--required"
|
||||
>*</abbr>).</p>
|
||||
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="usa-button usa-button--unstyled display-block" name="confirm-ds">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||
</svg><span class="margin-left-05">Add new record</span>
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>In order to enable DNSSEC, you must first configure it with your DNS hosting service.</p>
|
||||
|
||||
<p>Enter the values given by your DNS provider for DS Data.</p>
|
||||
|
||||
{% include "includes/required_fields.html" %}
|
||||
|
||||
<form class="usa-form usa-form--extra-large" method="post" novalidate id="form-container">
|
||||
|
@ -104,20 +81,40 @@
|
|||
</button>
|
||||
|
||||
<button
|
||||
id="save-ds-data"
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
>Save
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form aria-label="form to undo changes to the DS records">
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button usa-button--outline btn-cancel"
|
||||
class="usa-button usa-button--outline"
|
||||
name="btn-cancel-click"
|
||||
aria-label="Reset the data in the DS records to the registry state (undo changes)"
|
||||
>Cancel
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{% if trigger_modal %}
|
||||
<a
|
||||
id="ds-toggle-dnssec-alert"
|
||||
href="#toggle-dnssec-alert"
|
||||
class="usa-button usa-button--outline margin-top-1 display-none"
|
||||
aria-controls="toggle-dnssec-alert"
|
||||
data-open-modal
|
||||
>Trigger Disable DNSSEC Modal</a
|
||||
>
|
||||
{% endif %}
|
||||
{# Use data-force-action to take esc out of the equation and pass cancel_button_resets_ds_form to effectuate a reset in the view #}
|
||||
<div
|
||||
class="usa-modal"
|
||||
id="toggle-dnssec-alert"
|
||||
aria-labelledby="Are you sure you want to continue?"
|
||||
aria-describedby="Your DNSSEC records will be deleted from the registry."
|
||||
data-force-action
|
||||
>
|
||||
{% include 'includes/modal.html' with cancel_button_resets_ds_form=True modal_heading="Warning: You are about to delete all DS records on your domain" modal_description="To fully disable DNSSEC: In addition to deleting your DS records here you’ll also need to delete the DS records at your DNS host. To avoid causing your domain to appear offline you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button=modal_button|safe %}
|
||||
</div>
|
||||
|
||||
{% endblock %} {# domain_content #}
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
{% extends "domain_base.html" %}
|
||||
{% load static field_helpers url_helpers %}
|
||||
|
||||
{% block title %}Key Data | {{ domain.name }} | {% endblock %}
|
||||
|
||||
{% block domain_content %}
|
||||
{% for form in formset %}
|
||||
{% include "includes/form_errors.html" with form=form %}
|
||||
{% endfor %}
|
||||
|
||||
<h1>Key Data</h1>
|
||||
|
||||
{% if domain.dnssecdata is not None and domain.dnssecdata.dsData is not None %}
|
||||
<div class="usa-alert usa-alert--warning usa-alert--slim margin-bottom-3">
|
||||
<div class="usa-alert__body">
|
||||
<h4 class="usa-alert__heading">Warning, you cannot add Key Data</h4>
|
||||
<p class="usa-alert__text">
|
||||
You cannot add Key Data because you have already added DS Data. Delete your DS Data records in order to add Key Data.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% elif not dnssec_key_confirmed %}
|
||||
<p>In order to enable DNSSEC and add DS records, you must first configure it with your DNS hosting service. Your configuration will determine whether you need to add DS Data or Key Data. Contact your DNS hosting provider if you are unsure which record type to add.</p>
|
||||
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
||||
{% csrf_token %}
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
name="confirm-key"
|
||||
>Add DS Key record</button>
|
||||
</form>
|
||||
{% else %}
|
||||
|
||||
<p>Enter the values given by your DNS provider for DS Key Data.</p>
|
||||
{% include "includes/required_fields.html" %}
|
||||
|
||||
<form class="usa-form usa-form--extra-large" method="post" novalidate id="form-container">
|
||||
{% csrf_token %}
|
||||
{{ formset.management_form }}
|
||||
|
||||
{% for form in formset %}
|
||||
<fieldset class="ds-record">
|
||||
|
||||
<legend class="sr-only">DS Data record {{forloop.counter}}</legend>
|
||||
|
||||
<h2 class="margin-top-0">DS Data record {{forloop.counter}}</h2>
|
||||
|
||||
<div class="grid-row grid-gap-2 flex-end">
|
||||
<div class="tablet:grid-col-4">
|
||||
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.flag %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="tablet:grid-col-4">
|
||||
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.protocol %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="tablet:grid-col-4">
|
||||
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.algorithm %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-row">
|
||||
<div class="grid-col">
|
||||
{% with attr_required=True add_group_class="usa-form-group--unstyled-error" %}
|
||||
{% input_with_errors form.pub_key %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-row margin-top-2">
|
||||
<div class="grid-col">
|
||||
<button type="button" class="usa-button usa-button--unstyled display-block float-right-tablet delete-record">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||
</svg><span class="margin-left-05">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
{% endfor %}
|
||||
|
||||
<button type="button" class="usa-button usa-button--unstyled display-block margin-bottom-2" id="add-ds-form">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||
</svg><span class="margin-left-05">Add new record</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
>Save
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form aria-label="form to undo changes to the DS records">
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button usa-button--outline btn-cancel"
|
||||
name="btn-cancel-click"
|
||||
aria-label="Reset the data in the DS records to the registry state (undo changes)"
|
||||
>Cancel
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %} {# domain_content #}
|
|
@ -34,7 +34,7 @@
|
|||
>
|
||||
DNSSEC
|
||||
</a>
|
||||
{% if domain.dnssecdata is not None or request.path|startswith:url and request.path|endswith:'data' %}
|
||||
{% if domain.dnssecdata is not None or request.path|startswith:url and request.path|endswith:'dsdata' %}
|
||||
<ul class="usa-sidenav__sublist">
|
||||
<li class="usa-sidenav__item">
|
||||
{% url 'domain-dns-dnssec-dsdata' pk=domain.id as url %}
|
||||
|
@ -44,15 +44,6 @@
|
|||
DS Data
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="usa-sidenav__item">
|
||||
{% url 'domain-dns-dnssec-keydata' pk=domain.id as url %}
|
||||
<a href="{{ url }}"
|
||||
{% if request.path == url %}class="usa-current"{% endif %}
|
||||
>
|
||||
DS Key Data
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
|
@ -100,7 +91,7 @@
|
|||
<a href="{{ url }}"
|
||||
{% if request.path|startswith:url %}class="usa-current"{% endif %}
|
||||
>
|
||||
User management
|
||||
Domain managers
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -1,10 +1,23 @@
|
|||
{% extends "domain_base.html" %}
|
||||
{% load static %}
|
||||
{% load static url_helpers %}
|
||||
|
||||
{% block title %}User management | {{ domain.name }} | {% endblock %}
|
||||
{% block title %}Domain managers | {{ domain.name }} | {% endblock %}
|
||||
|
||||
{% block domain_content %}
|
||||
<h1>User management</h1>
|
||||
<h1>Domain managers</h1>
|
||||
|
||||
<p>
|
||||
Domain managers can update all information related to a domain within the
|
||||
.gov registrar, including contact details, authorizing official, security
|
||||
email, and DNS name servers.
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>There is no limit to the number of domain managers you can add.</li>
|
||||
<li>After adding a domain manager, an email invitation will be sent to that user with
|
||||
instructions on how to set up an account.</li>
|
||||
<li>To remove a domain manager, <a href="{% public_site_url 'contact/' %}" class="usa-link">contact us</a> for assistance.
|
||||
</ul>
|
||||
|
||||
{% if domain.permissions %}
|
||||
<section class="section--outlined">
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
{% load static form_helpers url_helpers %}
|
||||
|
||||
<div class="usa-modal__content">
|
||||
<div class="usa-modal__main">
|
||||
<h2 class="usa-modal__heading" id="modal-1-heading">
|
||||
|
@ -18,25 +20,60 @@
|
|||
</form>
|
||||
</li>
|
||||
<li class="usa-button-group__item">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||
data-close-modal
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{% comment %} The cancel button the DS form actually triggers a context change in the view,
|
||||
in addition to being a close modal hook {% endcomment %}
|
||||
{% if cancel_button_resets_ds_form %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||
name="btn-cancel-click"
|
||||
data-close-modal
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||
data-close-modal
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-modal__close"
|
||||
aria-label="Close this window"
|
||||
data-close-modal
|
||||
>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="/assets/img/sprite.svg#close"></use>
|
||||
</svg>
|
||||
</button>
|
||||
{% comment %} The cancel button the DS form actually triggers a context change in the view,
|
||||
in addition to being a close modal hook {% endcomment %}
|
||||
{% if cancel_button_resets_ds_form %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button usa-modal__close"
|
||||
aria-label="Close this window"
|
||||
name="btn-cancel-click"
|
||||
data-close-modal
|
||||
>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-modal__close"
|
||||
aria-label="Close this window"
|
||||
data-close-modal
|
||||
>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||
</svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import logging
|
||||
from django import template
|
||||
import re
|
||||
from registrar.models.domain_application import DomainApplication
|
||||
|
||||
register = template.Library()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register.filter(name="extract_value")
|
||||
|
@ -48,3 +51,16 @@ def contains_checkbox(html_list):
|
|||
if re.search(r'<input[^>]*type="checkbox"', html_string):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@register.filter
|
||||
def get_organization_long_name(organization_type):
|
||||
organization_choices_dict = dict(
|
||||
DomainApplication.OrganizationChoicesVerbose.choices
|
||||
)
|
||||
long_form_type = organization_choices_dict[organization_type]
|
||||
if long_form_type is None:
|
||||
logger.error("Organization type error, triggered by a template's custom filter")
|
||||
return "Error"
|
||||
|
||||
return long_form_type
|
||||
|
|
|
@ -774,12 +774,6 @@ class MockEppLib(TestCase):
|
|||
"digestType": 1,
|
||||
"digest": "ec0bdd990b39feead889f0ba613db4adecb4adec",
|
||||
}
|
||||
keyDataDict = {
|
||||
"flags": 257,
|
||||
"protocol": 3,
|
||||
"alg": 1,
|
||||
"pubKey": "AQPJ////4Q==",
|
||||
}
|
||||
dnssecExtensionWithDsData = extensions.DNSSECExtension(
|
||||
**{
|
||||
"dsData": [
|
||||
|
@ -795,11 +789,6 @@ class MockEppLib(TestCase):
|
|||
], # type: ignore
|
||||
}
|
||||
)
|
||||
dnssecExtensionWithKeyData = extensions.DNSSECExtension(
|
||||
**{
|
||||
"keyData": [common.DNSSECKeyData(**keyDataDict)], # type: ignore
|
||||
}
|
||||
)
|
||||
dnssecExtensionRemovingDsData = extensions.DNSSECExtension()
|
||||
|
||||
infoDomainHasIP = fakedEppObject(
|
||||
|
@ -951,10 +940,6 @@ class MockEppLib(TestCase):
|
|||
self.mockDataInfoDomain,
|
||||
self.dnssecExtensionWithMultDsData,
|
||||
),
|
||||
"dnssec-keydata.gov": (
|
||||
self.mockDataInfoDomain,
|
||||
self.dnssecExtensionWithKeyData,
|
||||
),
|
||||
"dnssec-none.gov": (self.mockDataInfoDomain, None),
|
||||
"my-nameserver.gov": (
|
||||
self.infoDomainTwoHosts
|
||||
|
|
|
@ -11,6 +11,7 @@ from registrar.admin import (
|
|||
ListHeaderAdmin,
|
||||
MyUserAdmin,
|
||||
AuditedAdmin,
|
||||
ContactAdmin,
|
||||
)
|
||||
from registrar.models import (
|
||||
Domain,
|
||||
|
@ -52,6 +53,26 @@ class TestDomainAdmin(MockEppLib):
|
|||
self.factory = RequestFactory()
|
||||
super().setUp()
|
||||
|
||||
def test_short_org_name_in_domains_list(self):
|
||||
"""
|
||||
Make sure the short name is displaying in admin on the list page
|
||||
"""
|
||||
self.client.force_login(self.superuser)
|
||||
application = completed_application(status=DomainApplication.IN_REVIEW)
|
||||
application.approve()
|
||||
|
||||
response = self.client.get("/admin/registrar/domain/")
|
||||
|
||||
# There are 3 template references to Federal (3) plus one reference in the table
|
||||
# for our actual application
|
||||
self.assertContains(response, "Federal", count=4)
|
||||
# This may be a bit more robust
|
||||
self.assertContains(
|
||||
response, '<td class="field-organization_type">Federal</td>', count=1
|
||||
)
|
||||
# Now let's make sure the long description does not exist
|
||||
self.assertNotContains(response, "Federal: an agency of the U.S. government")
|
||||
|
||||
@skip("Why did this test stop working, and is is a good test")
|
||||
def test_place_and_remove_hold(self):
|
||||
domain = create_ready_domain()
|
||||
|
@ -243,8 +264,11 @@ class TestDomainAdmin(MockEppLib):
|
|||
raise
|
||||
|
||||
def tearDown(self):
|
||||
User.objects.all().delete()
|
||||
super().tearDown()
|
||||
Domain.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
DomainApplication.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
|
||||
|
||||
class TestDomainApplicationAdminForm(TestCase):
|
||||
|
@ -300,6 +324,23 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
self.superuser = create_superuser()
|
||||
self.staffuser = create_user()
|
||||
|
||||
def test_short_org_name_in_applications_list(self):
|
||||
"""
|
||||
Make sure the short name is displaying in admin on the list page
|
||||
"""
|
||||
self.client.force_login(self.superuser)
|
||||
completed_application()
|
||||
response = self.client.get("/admin/registrar/domainapplication/")
|
||||
# There are 3 template references to Federal (3) plus one reference in the table
|
||||
# for our actual application
|
||||
self.assertContains(response, "Federal", count=4)
|
||||
# This may be a bit more robust
|
||||
self.assertContains(
|
||||
response, '<td class="field-organization_type">Federal</td>', count=1
|
||||
)
|
||||
# Now let's make sure the long description does not exist
|
||||
self.assertNotContains(response, "Federal: an agency of the U.S. government")
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_save_model_sends_submitted_email(self):
|
||||
# make sure there is no user with this email
|
||||
|
@ -620,9 +661,6 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
expected_fields = [
|
||||
"creator",
|
||||
"about_your_organization",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"zipcode",
|
||||
"requested_domain",
|
||||
"alternative_domains",
|
||||
"purpose",
|
||||
|
@ -1313,3 +1351,38 @@ class DomainSessionVariableTest(TestCase):
|
|||
{"_edit_domain": "true"},
|
||||
follow=True,
|
||||
)
|
||||
|
||||
|
||||
class ContactAdminTest(TestCase):
|
||||
def setUp(self):
|
||||
self.site = AdminSite()
|
||||
self.factory = RequestFactory()
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
self.admin = ContactAdmin(model=get_user_model(), admin_site=None)
|
||||
self.superuser = create_superuser()
|
||||
self.staffuser = create_user()
|
||||
|
||||
def test_readonly_when_restricted_staffuser(self):
|
||||
request = self.factory.get("/")
|
||||
request.user = self.staffuser
|
||||
|
||||
readonly_fields = self.admin.get_readonly_fields(request)
|
||||
|
||||
expected_fields = [
|
||||
"user",
|
||||
]
|
||||
|
||||
self.assertEqual(readonly_fields, expected_fields)
|
||||
|
||||
def test_readonly_when_restricted_superuser(self):
|
||||
request = self.factory.get("/")
|
||||
request.user = self.superuser
|
||||
|
||||
readonly_fields = self.admin.get_readonly_fields(request)
|
||||
|
||||
expected_fields = []
|
||||
|
||||
self.assertEqual(readonly_fields, expected_fields)
|
||||
|
||||
def tearDown(self):
|
||||
User.objects.all().delete()
|
||||
|
|
|
@ -36,7 +36,7 @@ class TestGroups(TestCase):
|
|||
# Define the expected permission codenames
|
||||
expected_permissions = [
|
||||
"view_logentry",
|
||||
"view_contact",
|
||||
"change_contact",
|
||||
"view_domain",
|
||||
"change_domainapplication",
|
||||
"change_domaininformation",
|
||||
|
@ -45,6 +45,7 @@ class TestGroups(TestCase):
|
|||
"change_draftdomain",
|
||||
"analyst_access_permission",
|
||||
"change_user",
|
||||
"change_website",
|
||||
]
|
||||
|
||||
# Get the codenames of actual permissions associated with the group
|
||||
|
|
|
@ -14,7 +14,8 @@ from registrar.models import (
|
|||
UserDomainRole,
|
||||
)
|
||||
|
||||
import boto3_mocking # type: ignore
|
||||
import boto3_mocking
|
||||
from registrar.models.transition_domain import TransitionDomain # type: ignore
|
||||
from .common import MockSESClient, less_console_noise, completed_application
|
||||
from django_fsm import TransitionNotAllowed
|
||||
|
||||
|
@ -612,3 +613,48 @@ class TestInvitations(TestCase):
|
|||
"""A new user's first_login callback retrieves their invitations."""
|
||||
self.user.first_login()
|
||||
self.assertTrue(UserDomainRole.objects.get(user=self.user, domain=self.domain))
|
||||
|
||||
|
||||
class TestUser(TestCase):
|
||||
"""For now, just test actions that
|
||||
occur on user login."""
|
||||
|
||||
def setUp(self):
|
||||
self.email = "mayor@igorville.gov"
|
||||
self.domain_name = "igorvilleInTransition.gov"
|
||||
self.user, _ = User.objects.get_or_create(email=self.email)
|
||||
|
||||
# clean out the roles each time
|
||||
UserDomainRole.objects.all().delete()
|
||||
|
||||
TransitionDomain.objects.get_or_create(
|
||||
username="mayor@igorville.gov", domain_name=self.domain_name
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
Domain.objects.all().delete()
|
||||
DomainInvitation.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
TransitionDomain.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
|
||||
def test_check_transition_domains_on_login(self):
|
||||
"""A new user's first_login callback checks transition domains.
|
||||
Makes DomainInformation object."""
|
||||
self.domain, _ = Domain.objects.get_or_create(name=self.domain_name)
|
||||
|
||||
self.user.first_login()
|
||||
self.assertTrue(DomainInformation.objects.get(domain=self.domain))
|
||||
|
||||
def test_check_transition_domains_without_domains_on_login(self):
|
||||
"""A new user's first_login callback checks transition domains.
|
||||
This test makes sure that in the event a domain does not exist
|
||||
for a given transition domain, both a domain and domain invitation
|
||||
are created."""
|
||||
self.user.first_login()
|
||||
self.assertTrue(Domain.objects.get(name=self.domain_name))
|
||||
|
||||
domain = Domain.objects.get(name=self.domain_name)
|
||||
self.assertTrue(DomainInvitation.objects.get(email=self.email, domain=domain))
|
||||
self.assertTrue(DomainInformation.objects.get(domain=domain))
|
||||
|
|
|
@ -1991,79 +1991,6 @@ class TestRegistrantDNSSEC(MockEppLib):
|
|||
|
||||
patcher.stop()
|
||||
|
||||
def test_user_adds_dnssec_keydata(self):
|
||||
"""
|
||||
Scenario: Registrant adds DNSSEC key data.
|
||||
Verify that both the setter and getter are functioning properly
|
||||
|
||||
This test verifies:
|
||||
1 - setter calls UpdateDomain command
|
||||
2 - setter adds the UpdateDNSSECExtension extension to the command
|
||||
3 - setter causes the getter to call info domain on next get from cache
|
||||
4 - getter properly parses dnssecdata from InfoDomain response and sets to cache
|
||||
|
||||
"""
|
||||
|
||||
# need to use a separate patcher and side_effect for this test, as
|
||||
# response from InfoDomain must be different for different iterations
|
||||
# of the same command
|
||||
def side_effect(_request, cleaned):
|
||||
if isinstance(_request, commands.InfoDomain):
|
||||
if mocked_send.call_count == 1:
|
||||
return MagicMock(res_data=[self.mockDataInfoDomain])
|
||||
else:
|
||||
return MagicMock(
|
||||
res_data=[self.mockDataInfoDomain],
|
||||
extensions=[self.dnssecExtensionWithKeyData],
|
||||
)
|
||||
else:
|
||||
return MagicMock(res_data=[self.mockDataInfoHosts])
|
||||
|
||||
patcher = patch("registrar.models.domain.registry.send")
|
||||
mocked_send = patcher.start()
|
||||
mocked_send.side_effect = side_effect
|
||||
|
||||
domain, _ = Domain.objects.get_or_create(name="dnssec-keydata.gov")
|
||||
|
||||
domain.dnssecdata = self.dnssecExtensionWithKeyData
|
||||
# get the DNS SEC extension added to the UpdateDomain command
|
||||
# and verify that it is properly sent
|
||||
# args[0] is the _request sent to registry
|
||||
args, _ = mocked_send.call_args
|
||||
# assert that the extension matches
|
||||
self.assertEquals(
|
||||
args[0].extensions[0],
|
||||
self.createUpdateExtension(self.dnssecExtensionWithKeyData),
|
||||
)
|
||||
# test that the dnssecdata getter is functioning properly
|
||||
dnssecdata_get = domain.dnssecdata
|
||||
mocked_send.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
commands.UpdateDomain(
|
||||
name="dnssec-keydata.gov",
|
||||
nsset=None,
|
||||
keyset=None,
|
||||
registrant=None,
|
||||
auth_info=None,
|
||||
),
|
||||
cleaned=True,
|
||||
),
|
||||
call(
|
||||
commands.InfoDomain(
|
||||
name="dnssec-keydata.gov",
|
||||
),
|
||||
cleaned=True,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEquals(
|
||||
dnssecdata_get.keyData, self.dnssecExtensionWithKeyData.keyData
|
||||
)
|
||||
|
||||
patcher.stop()
|
||||
|
||||
def test_update_is_unsuccessful(self):
|
||||
"""
|
||||
Scenario: An update to the dns data is unsuccessful
|
||||
|
|
|
@ -142,9 +142,12 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
|
||||
@boto3_mocking.patching
|
||||
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
|
||||
this test work.
|
||||
|
||||
This test also looks for the long organization name on the summary page.
|
||||
"""
|
||||
num_pages_tested = 0
|
||||
# elections, type_of_work, tribal_government, no_other_contacts
|
||||
|
@ -428,7 +431,8 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
review_form = review_page.form
|
||||
|
||||
# Review page contains all the previously entered data
|
||||
self.assertContains(review_page, "Federal")
|
||||
# Let's make sure the long org name is displayed
|
||||
self.assertContains(review_page, "Federal: an agency of the U.S. government")
|
||||
self.assertContains(review_page, "Executive")
|
||||
self.assertContains(review_page, "Testorg")
|
||||
self.assertContains(review_page, "address 1")
|
||||
|
@ -1066,6 +1070,26 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# page = self.app.get(url)
|
||||
# self.assertNotContains(page, "VALUE")
|
||||
|
||||
def test_long_org_name_in_application(self):
|
||||
"""
|
||||
Make sure the long name is displaying in the application form,
|
||||
org step
|
||||
"""
|
||||
request = self.app.get(reverse("application:")).follow()
|
||||
self.assertContains(request, "Federal: an agency of the U.S. government")
|
||||
|
||||
def test_long_org_name_in_application_manage(self):
|
||||
"""
|
||||
Make sure the long name is displaying in the application summary
|
||||
page (manage your application)
|
||||
"""
|
||||
completed_application(status=DomainApplication.SUBMITTED, user=self.user)
|
||||
home_page = self.app.get("/")
|
||||
self.assertContains(home_page, "city.gov")
|
||||
# click the "Edit" link
|
||||
detail_page = home_page.click("Manage")
|
||||
self.assertContains(detail_page, "Federal: an agency of the U.S. government")
|
||||
|
||||
|
||||
class TestWithDomainPermissions(TestWithUser):
|
||||
def setUp(self):
|
||||
|
@ -1082,7 +1106,6 @@ class TestWithDomainPermissions(TestWithUser):
|
|||
self.domain_multdsdata, _ = Domain.objects.get_or_create(
|
||||
name="dnssec-multdsdata.gov"
|
||||
)
|
||||
self.domain_keydata, _ = Domain.objects.get_or_create(name="dnssec-keydata.gov")
|
||||
# We could simply use domain (igorville) but this will be more readable in tests
|
||||
# that inherit this setUp
|
||||
self.domain_dnssec_none, _ = Domain.objects.get_or_create(
|
||||
|
@ -1099,9 +1122,6 @@ class TestWithDomainPermissions(TestWithUser):
|
|||
DomainInformation.objects.get_or_create(
|
||||
creator=self.user, domain=self.domain_multdsdata
|
||||
)
|
||||
DomainInformation.objects.get_or_create(
|
||||
creator=self.user, domain=self.domain_keydata
|
||||
)
|
||||
DomainInformation.objects.get_or_create(
|
||||
creator=self.user, domain=self.domain_dnssec_none
|
||||
)
|
||||
|
@ -1124,11 +1144,6 @@ class TestWithDomainPermissions(TestWithUser):
|
|||
domain=self.domain_multdsdata,
|
||||
role=UserDomainRole.Roles.MANAGER,
|
||||
)
|
||||
UserDomainRole.objects.get_or_create(
|
||||
user=self.user,
|
||||
domain=self.domain_keydata,
|
||||
role=UserDomainRole.Roles.MANAGER,
|
||||
)
|
||||
UserDomainRole.objects.get_or_create(
|
||||
user=self.user,
|
||||
domain=self.domain_dnssec_none,
|
||||
|
@ -1257,14 +1272,14 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest):
|
|||
self.assertContains(detail_page, "(1.2.3.4, 2.3.4.5)")
|
||||
|
||||
|
||||
class TestDomainUserManagement(TestDomainOverview):
|
||||
def test_domain_user_management(self):
|
||||
class TestDomainManagers(TestDomainOverview):
|
||||
def test_domain_managers(self):
|
||||
response = self.client.get(
|
||||
reverse("domain-users", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
self.assertContains(response, "User management")
|
||||
self.assertContains(response, "Domain managers")
|
||||
|
||||
def test_domain_user_management_add_link(self):
|
||||
def test_domain_managers_add_link(self):
|
||||
"""Button to get to user add page works."""
|
||||
management_page = self.app.get(
|
||||
reverse("domain-users", kwargs={"pk": self.domain.id})
|
||||
|
@ -1691,38 +1706,13 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
|
||||
def test_dnssec_page_refreshes_enable_button(self):
|
||||
"""DNSSEC overview page loads when domain has no DNSSEC data
|
||||
and shows a 'Enable DNSSEC' button. When button is clicked the template
|
||||
updates. When user navigates away then comes back to the page, the
|
||||
'Enable DNSSEC' button is shown again."""
|
||||
# home_page = self.app.get("/")
|
||||
and shows a 'Enable DNSSEC' button."""
|
||||
|
||||
page = self.client.get(
|
||||
reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
self.assertContains(page, "Enable DNSSEC")
|
||||
|
||||
# Prepare the data for the POST request
|
||||
post_data = {
|
||||
"enable_dnssec": "Enable DNSSEC",
|
||||
}
|
||||
updated_page = self.client.post(
|
||||
reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}),
|
||||
post_data,
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertEqual(updated_page.status_code, 200)
|
||||
|
||||
self.assertContains(updated_page, "Add DS Data")
|
||||
self.assertContains(updated_page, "Add Key Data")
|
||||
|
||||
self.app.get("/")
|
||||
|
||||
back_to_page = self.client.get(
|
||||
reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id})
|
||||
)
|
||||
self.assertContains(back_to_page, "Enable DNSSEC")
|
||||
|
||||
def test_dnssec_page_loads_with_data_in_domain(self):
|
||||
"""DNSSEC overview page loads when domain has DNSSEC data
|
||||
and the template contains a button to disable DNSSEC."""
|
||||
|
@ -1767,43 +1757,25 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
)
|
||||
self.assertContains(page, "DS Data record 1")
|
||||
|
||||
def test_ds_form_loads_with_key_data(self):
|
||||
"""DNSSEC Add DS Data page loads when there is
|
||||
domain DNSSEC KEY data and shows an alert"""
|
||||
|
||||
page = self.client.get(
|
||||
reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_keydata.id})
|
||||
def test_ds_data_form_modal(self):
|
||||
"""When user clicks on save, a modal pops up."""
|
||||
add_data_page = self.app.get(
|
||||
reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})
|
||||
)
|
||||
self.assertContains(page, "Warning, you cannot add DS Data")
|
||||
|
||||
def test_key_form_loads_with_no_domain_data(self):
|
||||
"""DNSSEC Add Key Data page loads when there is no
|
||||
domain DNSSEC data and shows a button to Add DS Key record"""
|
||||
|
||||
page = self.client.get(
|
||||
reverse(
|
||||
"domain-dns-dnssec-keydata", kwargs={"pk": self.domain_dnssec_none.id}
|
||||
)
|
||||
# Assert that a hidden trigger for the modal does not exist.
|
||||
# This hidden trigger will pop on the page when certain condition are met:
|
||||
# 1) Initial form contained DS data, 2) All data is deleted and form is
|
||||
# submitted.
|
||||
self.assertNotContains(add_data_page, "Trigger Disable DNSSEC Modal")
|
||||
# Simulate a delete all data
|
||||
form_data = {}
|
||||
response = self.client.post(
|
||||
reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}),
|
||||
data=form_data,
|
||||
)
|
||||
self.assertContains(page, "Add DS Key record")
|
||||
|
||||
def test_key_form_loads_with_key_data(self):
|
||||
"""DNSSEC Add Key Data page loads when there is
|
||||
domain DNSSEC Key data and shows the data"""
|
||||
|
||||
page = self.client.get(
|
||||
reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_keydata.id})
|
||||
)
|
||||
self.assertContains(page, "DS Data record 1")
|
||||
|
||||
def test_key_form_loads_with_ds_data(self):
|
||||
"""DNSSEC Add Key Data page loads when there is
|
||||
domain DNSSEC DS data and shows an alert"""
|
||||
|
||||
page = self.client.get(
|
||||
reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_dsdata.id})
|
||||
)
|
||||
self.assertContains(page, "Warning, you cannot add Key Data")
|
||||
self.assertEqual(response.status_code, 200) # Adjust status code as needed
|
||||
# Now check to see whether the JS trigger for the modal is present on the page
|
||||
self.assertContains(response, "Trigger Disable DNSSEC Modal")
|
||||
|
||||
def test_ds_data_form_submits(self):
|
||||
"""DS Data form submits successfully
|
||||
|
@ -1849,50 +1821,6 @@ class TestDomainDNSSEC(TestDomainOverview):
|
|||
# the field.
|
||||
self.assertContains(result, "Key tag is required", count=2, status_code=200)
|
||||
|
||||
def test_key_data_form_submits(self):
|
||||
"""Key Data form submits successfully
|
||||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
add_data_page = self.app.get(
|
||||
reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_keydata.id})
|
||||
)
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
with less_console_noise(): # swallow log warning message
|
||||
result = add_data_page.forms[0].submit()
|
||||
# form submission was a post, response should be a redirect
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(
|
||||
result["Location"],
|
||||
reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_keydata.id}),
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
page = result.follow()
|
||||
self.assertContains(
|
||||
page, "The Key Data records for this domain have been updated."
|
||||
)
|
||||
|
||||
def test_key_data_form_invalid(self):
|
||||
"""Key Data form errors with invalid data
|
||||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
add_data_page = self.app.get(
|
||||
reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.domain_keydata.id})
|
||||
)
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# first two nameservers are required, so if we empty one out we should
|
||||
# get a form error
|
||||
add_data_page.forms[0]["form-0-pub_key"] = ""
|
||||
with less_console_noise(): # swallow logged warning message
|
||||
result = add_data_page.forms[0].submit()
|
||||
# form submission was a post with an error, response should be a 200
|
||||
# error text appears twice, once at the top of the page, once around
|
||||
# the field.
|
||||
self.assertContains(result, "Pub key is required", count=2, status_code=200)
|
||||
|
||||
|
||||
class TestApplicationStatus(TestWithUser, WebTest):
|
||||
def setUp(self):
|
||||
|
|
|
@ -7,7 +7,6 @@ from .domain import (
|
|||
DomainNameserversView,
|
||||
DomainDNSSECView,
|
||||
DomainDsDataView,
|
||||
DomainKeyDataView,
|
||||
DomainYourContactInformationView,
|
||||
DomainSecurityEmailView,
|
||||
DomainUsersView,
|
||||
|
|
|
@ -10,8 +10,8 @@ import logging
|
|||
from django.contrib import messages
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.db import IntegrityError
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import redirect
|
||||
from django.template import RequestContext
|
||||
from django.urls import reverse
|
||||
from django.views.generic.edit import FormMixin
|
||||
|
||||
|
@ -34,8 +34,6 @@ from ..forms import (
|
|||
DomainDnssecForm,
|
||||
DomainDsdataFormset,
|
||||
DomainDsdataForm,
|
||||
DomainKeydataFormset,
|
||||
DomainKeydataForm,
|
||||
)
|
||||
|
||||
from epplibwrapper import (
|
||||
|
@ -293,8 +291,8 @@ class DomainDNSSECView(DomainFormBaseView):
|
|||
# Create HTML for the modal button
|
||||
modal_button = (
|
||||
'<button type="submit" '
|
||||
'class="usa-button" '
|
||||
'name="disable_dnssec">Disable DNSSEC</button>'
|
||||
'class="usa-button usa-button--secondary" '
|
||||
'name="disable_dnssec">Confirm</button>'
|
||||
)
|
||||
|
||||
context["modal_button"] = modal_button
|
||||
|
@ -319,12 +317,6 @@ class DomainDNSSECView(DomainFormBaseView):
|
|||
errmsg = "Error removing existing DNSSEC record(s)."
|
||||
logger.error(errmsg + ": " + err)
|
||||
messages.error(self.request, errmsg)
|
||||
request.session["dnssec_ds_confirmed"] = False
|
||||
request.session["dnssec_key_confirmed"] = False
|
||||
elif "enable_dnssec" in request.POST:
|
||||
request.session["dnssec_enabled"] = True
|
||||
request.session["dnssec_ds_confirmed"] = False
|
||||
request.session["dnssec_key_confirmed"] = False
|
||||
|
||||
return self.form_valid(form)
|
||||
|
||||
|
@ -341,24 +333,17 @@ class DomainDsDataView(DomainFormBaseView):
|
|||
dnssecdata: extensions.DNSSECExtension = self.object.dnssecdata
|
||||
initial_data = []
|
||||
|
||||
if dnssecdata is not None:
|
||||
if dnssecdata.keyData is not None:
|
||||
# TODO: Throw an error
|
||||
# Note: This is moot if we're
|
||||
# removing key data
|
||||
pass
|
||||
|
||||
if dnssecdata.dsData is not None:
|
||||
# Add existing nameservers as initial data
|
||||
initial_data.extend(
|
||||
{
|
||||
"key_tag": record.keyTag,
|
||||
"algorithm": record.alg,
|
||||
"digest_type": record.digestType,
|
||||
"digest": record.digest,
|
||||
}
|
||||
for record in dnssecdata.dsData
|
||||
)
|
||||
if dnssecdata is not None and dnssecdata.dsData is not None:
|
||||
# Add existing nameservers as initial data
|
||||
initial_data.extend(
|
||||
{
|
||||
"key_tag": record.keyTag,
|
||||
"algorithm": record.alg,
|
||||
"digest_type": record.digestType,
|
||||
"digest": record.digest,
|
||||
}
|
||||
for record in dnssecdata.dsData
|
||||
)
|
||||
|
||||
# Ensure at least 1 record, filled or empty
|
||||
while len(initial_data) == 0:
|
||||
|
@ -376,38 +361,49 @@ class DomainDsDataView(DomainFormBaseView):
|
|||
# use "formset" instead of "form" for the key
|
||||
context["formset"] = context.pop("form")
|
||||
|
||||
# set the dnssec_ds_confirmed flag in the context for this view
|
||||
# based either on the existence of DS Data in the domain,
|
||||
# or on the flag stored in the session
|
||||
dnssecdata: extensions.DNSSECExtension = self.object.dnssecdata
|
||||
|
||||
if dnssecdata is not None and dnssecdata.dsData is not None:
|
||||
self.request.session["dnssec_ds_confirmed"] = True
|
||||
|
||||
context["dnssec_ds_confirmed"] = self.request.session.get(
|
||||
"dnssec_ds_confirmed", False
|
||||
)
|
||||
return context
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Formset submission posts to this view."""
|
||||
self._get_domain(request)
|
||||
formset = self.get_form()
|
||||
override = False
|
||||
|
||||
if "confirm-ds" in request.POST:
|
||||
request.session["dnssec_ds_confirmed"] = True
|
||||
request.session["dnssec_key_confirmed"] = False
|
||||
return super().form_valid(formset)
|
||||
|
||||
# This is called by the form cancel button,
|
||||
# and also by the modal's X and cancel buttons
|
||||
if "btn-cancel-click" in request.POST:
|
||||
return redirect("/", {"formset": formset}, RequestContext(request))
|
||||
url = self.get_success_url()
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if formset.is_valid():
|
||||
# This is called by the Disable DNSSEC modal to override
|
||||
if "disable-override-click" in request.POST:
|
||||
override = True
|
||||
|
||||
# This is called when all DNSSEC data has been deleted and the
|
||||
# Save button is pressed
|
||||
if len(formset) == 0 and formset.initial != [{}] and override is False:
|
||||
# trigger the modal
|
||||
# get context data from super() rather than self
|
||||
# to preserve the context["form"]
|
||||
context = super().get_context_data(form=formset)
|
||||
context["trigger_modal"] = True
|
||||
# Create HTML for the modal button
|
||||
modal_button = (
|
||||
'<button type="submit" '
|
||||
'class="usa-button usa-button--secondary" '
|
||||
'name="disable-override-click">Delete all records</button>'
|
||||
)
|
||||
|
||||
# context to back out of a broken form on all fields delete
|
||||
context["modal_button"] = modal_button
|
||||
return self.render_to_response(context)
|
||||
|
||||
if formset.is_valid() or override:
|
||||
return self.form_valid(formset)
|
||||
else:
|
||||
return self.form_invalid(formset)
|
||||
|
||||
def form_valid(self, formset):
|
||||
def form_valid(self, formset, **kwargs):
|
||||
"""The formset is valid, perform something with it."""
|
||||
|
||||
# Set the dnssecdata from the formset
|
||||
|
@ -434,10 +430,12 @@ class DomainDsDataView(DomainFormBaseView):
|
|||
try:
|
||||
self.object.dnssecdata = dnssecdata
|
||||
except RegistryError as err:
|
||||
errmsg = "Error updating DNSSEC data in the registry."
|
||||
logger.error(errmsg)
|
||||
logger.error(err)
|
||||
messages.error(self.request, errmsg)
|
||||
if err.is_connection_error():
|
||||
messages.error(self.request, CANNOT_CONTACT_REGISTRY)
|
||||
logger.error(f"Registry connection error: {err}")
|
||||
else:
|
||||
messages.error(self.request, GENERIC_ERROR)
|
||||
logger.error(f"Registry error: {err}")
|
||||
return self.form_invalid(formset)
|
||||
else:
|
||||
messages.success(
|
||||
|
@ -447,123 +445,6 @@ class DomainDsDataView(DomainFormBaseView):
|
|||
return super().form_valid(formset)
|
||||
|
||||
|
||||
class DomainKeyDataView(DomainFormBaseView):
|
||||
"""Domain DNSSEC key data editing view."""
|
||||
|
||||
template_name = "domain_keydata.html"
|
||||
form_class = DomainKeydataFormset
|
||||
form = DomainKeydataForm
|
||||
|
||||
def get_initial(self):
|
||||
"""The initial value for the form (which is a formset here)."""
|
||||
dnssecdata: extensions.DNSSECExtension = self.object.dnssecdata
|
||||
initial_data = []
|
||||
|
||||
if dnssecdata is not None:
|
||||
if dnssecdata.dsData is not None:
|
||||
# TODO: Throw an error?
|
||||
# Note: this is moot if we're
|
||||
# removing Key data
|
||||
pass
|
||||
|
||||
if dnssecdata.keyData is not None:
|
||||
# Add existing keydata as initial data
|
||||
initial_data.extend(
|
||||
{
|
||||
"flag": record.flags,
|
||||
"protocol": record.protocol,
|
||||
"algorithm": record.alg,
|
||||
"pub_key": record.pubKey,
|
||||
}
|
||||
for record in dnssecdata.keyData
|
||||
)
|
||||
|
||||
# Ensure at least 1 record, filled or empty
|
||||
while len(initial_data) == 0:
|
||||
initial_data.append({})
|
||||
|
||||
return initial_data
|
||||
|
||||
def get_success_url(self):
|
||||
"""Redirect to the Key Data page for the domain."""
|
||||
return reverse("domain-dns-dnssec-keydata", kwargs={"pk": self.object.pk})
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Adjust context from FormMixin for formsets."""
|
||||
context = super().get_context_data(**kwargs)
|
||||
# use "formset" instead of "form" for the key
|
||||
context["formset"] = context.pop("form")
|
||||
|
||||
# set the dnssec_key_confirmed flag in the context for this view
|
||||
# based either on the existence of Key Data in the domain,
|
||||
# or on the flag stored in the session
|
||||
dnssecdata: extensions.DNSSECExtension = self.object.dnssecdata
|
||||
|
||||
if dnssecdata is not None and dnssecdata.keyData is not None:
|
||||
self.request.session["dnssec_key_confirmed"] = True
|
||||
|
||||
context["dnssec_key_confirmed"] = self.request.session.get(
|
||||
"dnssec_key_confirmed", False
|
||||
)
|
||||
return context
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Formset submission posts to this view."""
|
||||
self._get_domain(request)
|
||||
formset = self.get_form()
|
||||
|
||||
if "confirm-key" in request.POST:
|
||||
request.session["dnssec_key_confirmed"] = True
|
||||
request.session["dnssec_ds_confirmed"] = False
|
||||
self.object.save()
|
||||
return super().form_valid(formset)
|
||||
|
||||
if "btn-cancel-click" in request.POST:
|
||||
return redirect("/", {"formset": formset}, RequestContext(request))
|
||||
|
||||
if formset.is_valid():
|
||||
return self.form_valid(formset)
|
||||
else:
|
||||
return self.form_invalid(formset)
|
||||
|
||||
def form_valid(self, formset):
|
||||
"""The formset is valid, perform something with it."""
|
||||
|
||||
# Set the nameservers from the formset
|
||||
dnssecdata = extensions.DNSSECExtension()
|
||||
|
||||
for form in formset:
|
||||
try:
|
||||
# if 'delete' not in form.cleaned_data
|
||||
# or form.cleaned_data['delete'] == False:
|
||||
keyrecord = {
|
||||
"flags": int(form.cleaned_data["flag"]),
|
||||
"protocol": int(form.cleaned_data["protocol"]),
|
||||
"alg": int(form.cleaned_data["algorithm"]),
|
||||
"pubKey": form.cleaned_data["pub_key"],
|
||||
}
|
||||
if dnssecdata.keyData is None:
|
||||
dnssecdata.keyData = []
|
||||
dnssecdata.keyData.append(common.DNSSECKeyData(**keyrecord))
|
||||
except KeyError:
|
||||
# no server information in this field, skip it
|
||||
pass
|
||||
try:
|
||||
self.object.dnssecdata = dnssecdata
|
||||
except RegistryError as err:
|
||||
errmsg = "Error updating DNSSEC data in the registry."
|
||||
logger.error(errmsg)
|
||||
logger.error(err)
|
||||
messages.error(self.request, errmsg)
|
||||
return self.form_invalid(formset)
|
||||
else:
|
||||
messages.success(
|
||||
self.request, "The Key Data records for this domain have been updated."
|
||||
)
|
||||
# superclass has the redirect
|
||||
return super().form_valid(formset)
|
||||
|
||||
|
||||
class DomainYourContactInformationView(DomainFormBaseView):
|
||||
"""Domain your contact information editing view."""
|
||||
|
||||
|
@ -656,7 +537,7 @@ class DomainSecurityEmailView(DomainFormBaseView):
|
|||
|
||||
|
||||
class DomainUsersView(DomainBaseView):
|
||||
"""User management page in the domain details."""
|
||||
"""Domain managers page in the domain details."""
|
||||
|
||||
template_name = "domain_users.html"
|
||||
|
||||
|
|
|
@ -1,53 +1,61 @@
|
|||
-i https://pypi.python.org/simple
|
||||
asgiref==3.7.2 ; python_version >= '3.7'
|
||||
boto3==1.26.145
|
||||
botocore==1.29.145 ; python_version >= '3.7'
|
||||
cachetools==5.3.1
|
||||
certifi==2023.7.22 ; python_version >= '3.6'
|
||||
annotated-types==0.6.0; python_version >= '3.8'
|
||||
asgiref==3.7.2; python_version >= '3.7'
|
||||
boto3==1.28.66; python_version >= '3.7'
|
||||
botocore==1.31.66; python_version >= '3.7'
|
||||
cachetools==5.3.1; python_version >= '3.7'
|
||||
certifi==2023.7.22; python_version >= '3.6'
|
||||
cfenv==0.5.3
|
||||
cffi==1.15.1
|
||||
charset-normalizer==3.1.0 ; python_full_version >= '3.7.0'
|
||||
cryptography==41.0.4 ; python_version >= '3.7'
|
||||
defusedxml==0.7.1 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
dj-database-url==2.0.0
|
||||
cffi==1.16.0; python_version >= '3.8'
|
||||
charset-normalizer==3.3.0; python_full_version >= '3.7.0'
|
||||
cryptography==41.0.4; python_version >= '3.7'
|
||||
defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
dj-database-url==2.1.0
|
||||
dj-email-url==1.0.6
|
||||
django==4.2.3
|
||||
django-allow-cidr==0.6.0
|
||||
django-auditlog==2.3.0
|
||||
django==4.2.6; python_version >= '3.8'
|
||||
django-allow-cidr==0.7.1
|
||||
django-auditlog==2.3.0; python_version >= '3.7'
|
||||
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
|
||||
faker==18.10.0
|
||||
git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a3a9a345f0ef561c#egg=fred-epplib
|
||||
django-phonenumber-field[phonenumberslite]==7.2.0; python_version >= '3.8'
|
||||
django-widget-tweaks==1.5.0; python_version >= '3.8'
|
||||
environs[django]==9.5.0; python_version >= '3.6'
|
||||
faker==19.11.0; python_version >= '3.8'
|
||||
fred-epplib@ git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a3a9a345f0ef561c
|
||||
furl==2.1.3
|
||||
future==0.18.3 ; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
gunicorn==20.1.0
|
||||
idna==3.4 ; python_version >= '3.5'
|
||||
jmespath==1.0.1 ; python_version >= '3.7'
|
||||
lxml==4.9.2 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
mako==1.2.4 ; python_version >= '3.7'
|
||||
markupsafe==2.1.2 ; python_version >= '3.7'
|
||||
marshmallow==3.19.0 ; python_version >= '3.7'
|
||||
oic==1.6.0
|
||||
future==0.18.3; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
gevent==23.9.1; python_version >= '3.8'
|
||||
geventconnpool@ git+https://github.com/rasky/geventconnpool.git@1bbb93a714a331a069adf27265fe582d9ba7ecd4
|
||||
greenlet==3.0.0; python_version >= '3.7'
|
||||
gunicorn==21.2.0; python_version >= '3.5'
|
||||
idna==3.4; python_version >= '3.5'
|
||||
jmespath==1.0.1; python_version >= '3.7'
|
||||
lxml==4.9.3; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
mako==1.2.4; python_version >= '3.7'
|
||||
markupsafe==2.1.3; python_version >= '3.7'
|
||||
marshmallow==3.20.1; python_version >= '3.8'
|
||||
oic==1.6.1; python_version ~= '3.7'
|
||||
orderedmultidict==1.0.1
|
||||
packaging==23.1 ; python_version >= '3.7'
|
||||
phonenumberslite==8.13.13
|
||||
psycopg2-binary==2.9.6
|
||||
packaging==23.2; python_version >= '3.7'
|
||||
phonenumberslite==8.13.23
|
||||
psycopg2-binary==2.9.9; python_version >= '3.7'
|
||||
pycparser==2.21
|
||||
pycryptodomex==3.18.0
|
||||
pydantic==1.10.8 ; python_version >= '3.7'
|
||||
pycryptodomex==3.19.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
pydantic==2.4.2; python_version >= '3.7'
|
||||
pydantic-core==2.10.1; python_version >= '3.7'
|
||||
pydantic-settings==2.0.3; python_version >= '3.7'
|
||||
pyjwkest==1.4.2
|
||||
python-dateutil==2.8.2 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
python-dotenv==1.0.0 ; python_version >= '3.8'
|
||||
requests==2.31.0
|
||||
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.3
|
||||
urllib3==1.26.17 ; 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
|
||||
python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
python-dotenv==1.0.0; python_version >= '3.8'
|
||||
requests==2.31.0; python_version >= '3.7'
|
||||
s3transfer==0.7.0; python_version >= '3.7'
|
||||
setuptools==68.2.2; python_version >= '3.8'
|
||||
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.8.0; python_version >= '3.8'
|
||||
urllib3==2.0.7; python_version >= '3.7'
|
||||
whitenoise==6.6.0; python_version >= '3.8'
|
||||
zope.event==5.0; python_version >= '3.7'
|
||||
zope.interface==6.1; python_version >= '3.7'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue