Merge branch 'main' into dk/1208-dnssec-addtl-items

This commit is contained in:
David Kennedy 2023-11-21 12:50:24 -05:00
commit 5145e3782e
No known key found for this signature in database
GPG key ID: 6528A5386E66B96B
22 changed files with 269 additions and 160 deletions

View file

@ -106,6 +106,7 @@ class EPPLibWrapper:
# Flag that the pool is frozen,
# then restart the pool.
self.pool_status.pool_hanging = True
logger.error("Pool timed out")
self.start_connection_pool()
except (ValueError, ParsingError) as err:
message = f"{cmd_type} failed to execute due to some syntax error."
@ -174,6 +175,7 @@ class EPPLibWrapper:
def _create_pool(self, client, login, options):
"""Creates and returns new pool instance"""
logger.info("New pool was created")
return EPPConnectionPool(client, login, options)
def start_connection_pool(self, restart_pool_if_exists=True):
@ -187,7 +189,7 @@ class EPPLibWrapper:
# 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")
logger.warning("start_connection_pool() -> Cannot contact the Registry")
self.pool_status.connection_success = False
else:
self.pool_status.connection_success = True
@ -197,6 +199,7 @@ class EPPLibWrapper:
if self._pool is not None and restart_pool_if_exists:
logger.info("Connection pool restarting...")
self.kill_pool()
logger.info("Old pool killed")
self._pool = self._create_pool(self._client, self._login, self.pool_options)
@ -221,6 +224,7 @@ class EPPLibWrapper:
credentials are valid, and/or if the Registrar
can be contacted
"""
# This is closed in test_connection_success
socket = Socket(self._client, self._login)
can_login = False

View file

@ -31,6 +31,7 @@ class Socket:
def connect(self):
"""Use epplib to connect."""
logger.info("Opening socket on connection pool")
self.client.connect()
response = self.client.send(self.login)
if self.is_login_error(response.code):
@ -40,11 +41,13 @@ class Socket:
def disconnect(self):
"""Close the connection."""
logger.info("Closing socket on connection pool")
try:
self.client.send(commands.Logout())
self.client.close()
except Exception:
except Exception as err:
logger.warning("Connection to registry was not cleanly closed.")
logger.error(err)
def send(self, command):
"""Sends a command to the registry.
@ -77,19 +80,17 @@ class Socket:
try:
self.client.connect()
response = self.client.send(self.login)
except LoginError as err:
if err.should_retry() and counter < 10:
except (LoginError, OSError) as err:
logger.error(err)
should_retry = True
if isinstance(err, LoginError):
should_retry = err.should_retry()
if 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")
@ -97,3 +98,5 @@ class Socket:
# Otherwise, just return true
return True
finally:
self.disconnect()

View file

@ -125,10 +125,14 @@ class TestConnectionPool(TestCase):
xml = (location).read_bytes()
return xml
def do_nothing(command):
pass
# 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(EPPConnectionPool, "kill_all_connections", do_nothing))
stack.enter_context(patch.object(SocketTransport, "send", self.fake_send))
stack.enter_context(patch.object(SocketTransport, "receive", fake_receive))
# Restart the connection pool

View file

@ -98,13 +98,17 @@ class EPPConnectionPool(ConnectionPool):
"""Kills all active connections in the pool."""
try:
if len(self.conn) > 0 or len(self.greenlets) > 0:
logger.info("Attempting to kill connections")
gevent.killall(self.greenlets)
self.greenlets.clear()
for connection in self.conn:
connection.disconnect()
self.conn.clear()
# Clear the semaphore
self.lock = BoundedSemaphore(self.size)
logger.info("Finished killing connections")
else:
logger.info("No connections to kill.")
except Exception as err:

View file

@ -136,6 +136,8 @@ MIDDLEWARE = [
"allow_cidr.middleware.AllowCIDRMiddleware",
# django-cors-headers: listen to cors responses
"corsheaders.middleware.CorsMiddleware",
# custom middleware to stop caching from CloudFront
"registrar.no_cache_middleware.NoCacheMiddleware",
# serve static assets in production
"whitenoise.middleware.WhiteNoiseMiddleware",
# provide security enhancements to the request/response cycle
@ -617,6 +619,8 @@ SECURE_SSL_REDIRECT = True
ALLOWED_HOSTS = [
"getgov-stable.app.cloud.gov",
"getgov-staging.app.cloud.gov",
"getgov-development.app.cloud.gov",
"getgov-ky.app.cloud.gov",
"getgov-es.app.cloud.gov",
"getgov-nl.app.cloud.gov",
"getgov-rh.app.cloud.gov",

View file

@ -5,7 +5,6 @@ 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
@ -97,51 +96,6 @@ class User(AbstractUser):
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 on_each_login(self):
"""Callback each time the user is authenticated.
@ -152,17 +106,6 @@ class User(AbstractUser):
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:

View file

@ -0,0 +1,18 @@
"""Middleware to add Cache-control: no-cache to every response.
Used to force Cloudfront caching to leave us alone while we develop
better caching responses.
"""
class NoCacheMiddleware:
"""Middleware to add a single header to every response."""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
response["Cache-Control"] = "no-cache"
return response

View file

@ -11,23 +11,18 @@
<h1>
{% translate "You are not authorized to view this page" %}
</h1>
<h2>
{% translate "Status 401" %}
</h2>
{% if friendly_message %}
<p>{{ friendly_message }}</p>
{% else %}
<p>{% translate "Authorization failed." %}</p>
{% endif %}
<p>
You must be an authorized user and need to be signed in to view this page.
Would you like to <a href="{% url 'login' %}"> try logging in again?</a>
You must be an authorized user and signed in to view this page. If you are an authorized user,
<strong><a href="{% url 'login' %}"> try signing in again</a>.</strong>
</p>
<p>
If you'd like help with this error <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'contact/' %}">contact us</a>.
</p>
If you'd like help with this error <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'contact/' %}">contact us</a>.</p>
{% if log_identifier %}
<p>Here's a unique identifier for this error.</p>
@ -35,6 +30,7 @@
<p>{% translate "Please include it if you contact us." %}</p>
{% endif %}
</div>
<div class="tablet:grid-col-4">
<img
src="{% static 'img/registrar/dotgov_401_illo.svg' %}"
@ -43,4 +39,4 @@
</div>
</div>
</main>
{% endblock %}
{% endblock %}

View file

@ -4,6 +4,7 @@
{% block title %}Security email | {{ domain.name }} | {% endblock %}
{% block domain_content %}
{% include "includes/form_errors.html" with form=form %}
<h1>Security email</h1>

View file

@ -4,6 +4,7 @@
{% block title %}Your contact information | {{ domain.name }} | {% endblock %}
{% block domain_content %}
{% include "includes/form_errors.html" with form=form %}
<h1>Your contact information</h1>

View file

@ -11,12 +11,22 @@ If youre not affiliated with the above domain{% if domains|length > 1 %}s{% e
CREATE A LOGIN.GOV ACCOUNT
You cant use your old credentials to access the new registrar. Access is now managed through Login.gov, a simple and secure process for signing into many government services with one account. Follow these steps to create your Login.gov account <https://login.gov/help/get-started/create-your-account/>.
You cant use your old credentials to access the new registrar. Access is now managed through Login.gov, a simple and secure process for signing in to many government services with one account.
When creating an account, youll need to provide the same email address you used to log in to the old registrar. That will ensure your domains are linked to your Login.gov account.
When creating a Login.gov account, youll need to provide the same email address you used to sign in to the old registrar. That will link your domain{% if domains|length > 1 %}s{% endif %} to your account.
If you need help finding the email address you used in the past, let us know in a reply to this email.
YOU MUST VERIFY YOUR IDENTITY WITH LOGIN.GOV
We require you to verify your identity with Login.gov as part of the account creation process. This is an extra layer of security that requires you to prove you are you, and not someone pretending to be you.
When you try to access the registrar with your Login.gov account, well ask you to verify your identity if you havent already. Youll only have to verify your identity once. Youll need a state-issued ID, a Social Security number, and a phone number for identity verification.
Follow these steps to create your Login.gov account <https://login.gov/help/get-started/create-your-account/>.
Read more about verifying your identity with Login.gov <https://login.gov/help/verify-your-identity/how-to-verify-your-identity/>.
CHECK YOUR .GOV DOMAIN CONTACTS
This is a good time to check who has access to your .gov domain{% if domains|length > 1 %}s{% endif %}. The admin, technical, and billing contacts listed for your domain{% if domains|length > 1 %}s{% endif %} in our old system also received this email. In our new registrar, these contacts are all considered “domain managers.” We no longer have the admin, technical, and billing roles, and you arent limited to three domain managers like in the old system.

View file

@ -627,22 +627,10 @@ class TestUser(TestCase):
TransitionDomain.objects.all().delete()
User.objects.all().delete()
def test_check_transition_domains_on_login(self):
"""A user's on_each_login callback checks transition domains.
Makes DomainInformation object."""
self.domain, _ = Domain.objects.get_or_create(name=self.domain_name)
self.user.on_each_login()
self.assertTrue(DomainInformation.objects.get(domain=self.domain))
def test_check_transition_domains_without_domains_on_login(self):
"""A user's on_each_login callback checks transition domains.
"""A user's on_each_login callback does not check 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.on_each_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))
self.assertFalse(Domain.objects.filter(name=self.domain_name).exists())

View file

@ -62,6 +62,9 @@
10038 OUTOFSCOPE http://app:8080/delete
10038 OUTOFSCOPE http://app:8080/withdraw
10038 OUTOFSCOPE http://app:8080/withdrawconfirmed
10038 OUTOFSCOPE http://app:8080/dns
10038 OUTOFSCOPE http://app:8080/dnssec
10038 OUTOFSCOPE http://app:8080/dns/dnssec
# This URL always returns 404, so include it as well.
10038 OUTOFSCOPE http://app:8080/todo
# OIDC isn't configured in the test environment and DEBUG=True so this gives a 500 without CSP headers