From ce7cfc1a53c9f222e42d9129eff9821f1dc43406 Mon Sep 17 00:00:00 2001 From: Seamus Johnston Date: Fri, 21 Apr 2023 09:08:47 -0500 Subject: [PATCH 1/5] Include epplibwrapper module --- .../runbooks/rotate_application_secrets.md | 35 +++++ src/.flake8 | 2 +- src/docker-compose.yml | 10 ++ src/epp/__init__.py | 0 src/epp/mock_epp.py | 40 ------ src/epplibwrapper/__init__.py | 48 +++++++ src/epplibwrapper/cert.py | 34 +++++ src/epplibwrapper/client.py | 125 ++++++++++++++++++ src/epplibwrapper/errors.py | 6 + src/epplibwrapper/socket.py | 37 ++++++ src/registrar/config/settings.py | 27 +++- src/registrar/models/domain.py | 7 +- src/registrar/tests/test_models.py | 1 + src/run.sh | 2 +- 14 files changed, 325 insertions(+), 49 deletions(-) delete mode 100644 src/epp/__init__.py delete mode 100644 src/epp/mock_epp.py create mode 100644 src/epplibwrapper/__init__.py create mode 100644 src/epplibwrapper/cert.py create mode 100644 src/epplibwrapper/client.py create mode 100644 src/epplibwrapper/errors.py create mode 100644 src/epplibwrapper/socket.py diff --git a/docs/operations/runbooks/rotate_application_secrets.md b/docs/operations/runbooks/rotate_application_secrets.md index dd0d367fc..4ce146ea8 100644 --- a/docs/operations/runbooks/rotate_application_secrets.md +++ b/docs/operations/runbooks/rotate_application_secrets.md @@ -65,3 +65,38 @@ You also need to upload the `public.crt` key if recently created to the login.go To access the AWS Simple Email Service, we need credentials from the CISA AWS account for an IAM user who has limited access to only SES. Those credentials need to be specified in the environment. + +## REGISTRY_CL_ID and REGISTRY_PASSWORD + +These are the login credentials for accessing the registry. + +## REGISTRY_CERT and REGISTRY_KEY and REGISTRY_KEY_PASSPHRASE + +These are the client certificate and its private key used to identify the registrar to the registry during the establishment of a TCP connection. + +The private key is protected by a passphrase for safer transport and storage. + +These were generated with: + +```bash +openssl genpkey -out client.key \ + -algorithm EC -pkeyopt ec_paramgen_curve:P-256 \ + -aes-256-cbc +openssl req -new -x509 -days 365 \ + -key client.key -out client.crt \ + -subj "/C=US/ST=DC/L=Washington/O=GSA/OU=18F/CN=GOV Prototype Registrar" + +``` + +Encode them using: + +```bash +base64 client.key +base64 client.crt +``` + +You'll need to give the new certificate to the registry vendor _before_ rotating it in production. + +## REGISTRY_HOSTNAME + +This is the hostname at which the registry can be found. diff --git a/src/.flake8 b/src/.flake8 index e1ca2cc9a..db9f3094a 100644 --- a/src/.flake8 +++ b/src/.flake8 @@ -2,6 +2,6 @@ max-line-length = 88 max-complexity = 10 extend-ignore = E203 -per-file-ignores = __init__.py:F401,F403 +per-file-ignores = __init__.py:F401,F403,E402 # migrations are auto-generated and often break rules exclude=registrar/migrations/* diff --git a/src/docker-compose.yml b/src/docker-compose.yml index 693cad6c0..e2b080716 100644 --- a/src/docker-compose.yml +++ b/src/docker-compose.yml @@ -33,6 +33,16 @@ services: # AWS credentials - AWS_ACCESS_KEY_ID - AWS_SECRET_ACCESS_KEY + # Set a username for accessing the registry + - REGISTRY_CL_ID + # Set a password for accessing the registry + - REGISTRY_PASSWORD + # Set a private certifcate for accessing the registry + - REGISTRY_CERT + # Set a private certifcate's key for accessing the registry + - REGISTRY_KEY + # Set a URI for accessing the registry + - REGISTRY_HOSTNAME stdin_open: true tty: true ports: diff --git a/src/epp/__init__.py b/src/epp/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/epp/mock_epp.py b/src/epp/mock_epp.py deleted file mode 100644 index 7d1d1d84e..000000000 --- a/src/epp/mock_epp.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -This file defines a number of mock functions which can be used to simulate -communication with the registry until that integration is implemented. -""" -from datetime import datetime - - -def domain_check(_): - """Is domain available for registration?""" - return True - - -def domain_info(domain): - """What does the registry know about this domain?""" - return { - "name": domain, - "roid": "EXAMPLE1-REP", - "status": ["ok"], - "registrant": "jd1234", - "contact": { - "admin": "sh8013", - "tech": None, - }, - "ns": { - f"ns1.{domain}", - f"ns2.{domain}", - }, - "host": [ - f"ns1.{domain}", - f"ns2.{domain}", - ], - "sponsor": "ClientX", - "creator": "ClientY", - # TODO: think about timezones - "creation_date": datetime.today(), - "updator": "ClientX", - "last_update_date": datetime.today(), - "expiration_date": datetime.today(), - "last_transfer_date": datetime.today(), - } diff --git a/src/epplibwrapper/__init__.py b/src/epplibwrapper/__init__.py new file mode 100644 index 000000000..29061da21 --- /dev/null +++ b/src/epplibwrapper/__init__.py @@ -0,0 +1,48 @@ +import logging +from types import SimpleNamespace + +try: + from epplib import constants +except ImportError: + pass + +logger = logging.getLogger(__name__) + +NAMESPACE = SimpleNamespace( + EPP="urn:ietf:params:xml:ns:epp-1.0", + XSI="http://www.w3.org/2001/XMLSchema-instance", + FRED="noop", + NIC_CONTACT="urn:ietf:params:xml:ns:contact-1.0", + NIC_DOMAIN="urn:ietf:params:xml:ns:domain-1.0", + NIC_ENUMVAL="noop", + NIC_EXTRA_ADDR="noop", + NIC_HOST="urn:ietf:params:xml:ns:host-1.0", + NIC_KEYSET="noop", + NIC_NSSET="noop", +) + +SCHEMA_LOCATION = SimpleNamespace( + XSI="urn:ietf:params:xml:ns:epp-1.0 epp-1.0.xsd", + FRED="noop fred-1.5.0.xsd", + NIC_CONTACT="urn:ietf:params:xml:ns:contact-1.0 contact-1.0.xsd", + NIC_DOMAIN="urn:ietf:params:xml:ns:domain-1.0 domain-1.0.xsd", + NIC_ENUMVAL="noop enumval-1.2.0.xsd", + NIC_EXTRA_ADDR="noop extra-addr-1.0.0.xsd", + NIC_HOST="urn:ietf:params:xml:ns:host-1.0 host-1.0.xsd", + NIC_KEYSET="noop keyset-1.3.2.xsd", + NIC_NSSET="noop nsset-1.2.2.xsd", +) + +try: + constants.NAMESPACE = NAMESPACE + constants.SCHEMA_LOCATION = SCHEMA_LOCATION +except NameError: + pass + +# Attn: these imports should NOT be at the top of the file +from .client import CLIENT, commands + +__all__ = [ + "CLIENT", + "commands", +] diff --git a/src/epplibwrapper/cert.py b/src/epplibwrapper/cert.py new file mode 100644 index 000000000..15ff16c06 --- /dev/null +++ b/src/epplibwrapper/cert.py @@ -0,0 +1,34 @@ +import os +import tempfile + +from django.conf import settings + + +class Cert: + """ + Location of client certificate as written to disk. + + This is needed because the certificate is stored as an environment + variable but Python's ssl library requires a file. + """ + + def __init__(self, data=settings.SECRET_REGISTRY_CERT) -> None: + self.filename = self._write(data) + + def __del__(self): + """Remove the files when this object is garbage collected.""" + os.unlink(self.filename) + + def _write(self, data) -> str: + """Write data to a secure tempfile. Returns the path.""" + _, path = tempfile.mkstemp() + with open(path, "wb") as file: + file.write(data) + return path + + +class Key(Cert): + """Location of private key as written to disk.""" + + def __init__(self) -> None: + super().__init__(data=settings.SECRET_REGISTRY_KEY) diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py new file mode 100644 index 000000000..d7f7068ca --- /dev/null +++ b/src/epplibwrapper/client.py @@ -0,0 +1,125 @@ +import logging +from time import sleep + +try: + from epplib.client import Client + from epplib import commands + from epplib.exceptions import TransportError, ParsingError + from epplib.transport import SocketTransport +except ImportError: + pass + +from django.conf import settings + +from .cert import Cert, Key +from .errors import LoginError, RegistryError +from .socket import Socket + +logger = logging.getLogger(__name__) + +try: + # Write cert and key to disk + CERT = Cert() + KEY = Key() +except Exception as err: + CERT = None # type: ignore + KEY = None # type: ignore + logger.warning(err) + logger.warning( + "Problem with client certificate. Registrar cannot contact registry." + ) + + +class EPPLibWrapper: + """ + A wrapper over epplib's client. + + ATTN: This should not be used directly. Use `Domain` from domain.py. + """ + + def __init__(self) -> 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, + password=settings.SECRET_REGISTRY_PASSWORD, + obj_uris=[ + "urn:ietf:params:xml:ns:domain-1.0", + "urn:ietf:params:xml:ns:contact-1.0", + ], + ) + # establish a client object with a TCP socket transport + self._client = Client( + SocketTransport( + settings.SECRET_REGISTRY_HOSTNAME, + cert_file=CERT.filename, + key_file=KEY.filename, + 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) + + def _send(self, command): + """Helper function used by `send`.""" + try: + with self._connect as wire: + response = wire.send(command) + except (ValueError, ParsingError) as err: + logger.debug(err) + logger.warning( + "%s failed to execute due to some syntax error." + % command.__class__.__name__ + ) + raise RegistryError() from err + except TransportError as err: + logger.debug(err) + logger.warning( + "%s failed to execute due to a connection error." + % command.__class__.__name__ + ) + raise RegistryError() from err + except LoginError as err: + logger.debug(err) + logger.warning( + "%s failed to execute due to a registry login error." + % command.__class__.__name__ + ) + raise RegistryError() from err + except Exception as err: + logger.debug(err) + logger.warning( + "%s failed to execute due to an unknown error." + % command.__class__.__name__ + ) + raise RegistryError() from err + else: + if response.code >= 2000: + raise RegistryError(response.msg) + else: + return response + + def send(self, command): + """Login, send the command, then close the connection. Tries 3 times.""" + counter = 0 # we'll try 3 times + while True: + try: + return self._send(command) + except RegistryError as err: + if counter == 3: # don't try again + raise err + else: + counter += 1 + sleep((counter * 50) / 1000) # sleep 50 ms to 150 ms + + +try: + # Initialize epplib + CLIENT = EPPLibWrapper() + logger.debug("registry client initialized") +except Exception as err: + CLIENT = None # type: ignore + logger.warning(err) + logger.warning("Unable to configure epplib. Registrar cannot contact registry.") diff --git a/src/epplibwrapper/errors.py b/src/epplibwrapper/errors.py new file mode 100644 index 000000000..c3028e0c4 --- /dev/null +++ b/src/epplibwrapper/errors.py @@ -0,0 +1,6 @@ +class RegistryError(Exception): + pass + + +class LoginError(RegistryError): + pass diff --git a/src/epplibwrapper/socket.py b/src/epplibwrapper/socket.py new file mode 100644 index 000000000..5c9acce79 --- /dev/null +++ b/src/epplibwrapper/socket.py @@ -0,0 +1,37 @@ +import logging + +try: + from epplib import commands +except ImportError: + pass + +from .errors import LoginError + + +logger = logging.getLogger(__name__) + + +class Socket: + """Context manager which establishes a TCP connection with registry.""" + + def __init__(self, client, login) -> None: + """Save the epplib client and login details.""" + self.client = client + self.login = login + + def __enter__(self): + """Use epplib to connect.""" + self.client.connect() + response = self.client.send(self.login) + if response.code >= 2000: + self.client.close() + raise LoginError(response.msg) + return self.client + + def __exit__(self, *args, **kwargs): + """Close the connection.""" + try: + self.client.send(commands.Logout()) + self.client.close() + except Exception: + logger.warning("Connection to registry was not cleanly closed.") diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 2947e2a70..6da8a1c83 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -55,11 +55,18 @@ secret_key = secret("DJANGO_SECRET_KEY") secret_aws_ses_key_id = secret("AWS_ACCESS_KEY_ID", None) secret_aws_ses_key = secret("AWS_SECRET_ACCESS_KEY", None) +secret_registry_cl_id = secret("REGISTRY_CL_ID") +secret_registry_password = secret("REGISTRY_PASSWORD") +secret_registry_cert = b64decode(secret("REGISTRY_CERT", "")) +secret_registry_key = b64decode(secret("REGISTRY_KEY", "")) +secret_registry_key_passphrase = secret("REGISTRY_KEY_PASSPHRASE", "") +secret_registry_hostname = secret("REGISTRY_HOSTNAME") # region: Basic Django Config-----------------------------------------------### # Build paths inside the project like this: BASE_DIR / "subdir". -BASE_DIR = path.resolve().parent.parent +# (settings.py is in `src/registrar/config/`: BASE_DIR is `src/`) +BASE_DIR = path.resolve().parent.parent.parent # SECURITY WARNING: don't run with debug turned on in production! DEBUG = env_debug @@ -156,16 +163,17 @@ WSGI_APPLICATION = "registrar.config.wsgi.application" # will place static files for deployment. # Do not use this directory for permanent storage - # it is for Django! -STATIC_ROOT = BASE_DIR / "public" +STATIC_ROOT = BASE_DIR / "registrar" / "public" STATICFILES_DIRS = [ - BASE_DIR / "assets", + BASE_DIR / "registrar" / "assets", + BASE_DIR / "epplibwrapper" / "assets", ] TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [BASE_DIR / "templates"], + "DIRS": [BASE_DIR / "registrar" / "templates"], # look for templates inside installed apps # required by django-debug-toolbar "APP_DIRS": True, @@ -496,6 +504,17 @@ ROOT_URLCONF = "registrar.config.urls" # Must be relative and end with "/" STATIC_URL = "public/" +# endregion +# region: Registry----------------------------------------------------------### + +# SECURITY WARNING: keep all registry variables in production secret! +SECRET_REGISTRY_CL_ID = secret_registry_cl_id +SECRET_REGISTRY_PASSWORD = secret_registry_password +SECRET_REGISTRY_CERT = secret_registry_cert +SECRET_REGISTRY_KEY = secret_registry_key +SECRET_REGISTRY_KEY_PASSPHRASE = secret_registry_key_passphrase +SECRET_REGISTRY_HOSTNAME = secret_registry_hostname + # endregion # region: Security and Privacy----------------------------------------------### diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 09b0fd211..b985c0629 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -7,7 +7,8 @@ from django.db import models from django_fsm import FSMField, transition # type: ignore from api.views import in_domains -from epp.mock_epp import domain_info, domain_check + +# from epplibwrapper import CLIENT as registry, commands from registrar.utility import errors from .utility.time_stamped_model import TimeStampedModel @@ -129,7 +130,7 @@ class Domain(TimeStampedModel): """Check if a domain is available. Not implemented. Returns a dummy value for testing.""" - return domain_check(domain) + return False # domain_check(domain) def transfer(self): """Going somewhere. Not implemented.""" @@ -146,7 +147,7 @@ class Domain(TimeStampedModel): if not hasattr(self, "info"): try: # get info from registry - self.info = domain_info(self.name) + self.info = {} # domain_info(self.name) except Exception as e: logger.error(e) # TODO: back off error handling diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 42b8803c3..784920ec5 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -118,6 +118,7 @@ class TestDomain(TestCase): domain = Domain.objects.create(name="igorville.gov") self.assertEqual(domain.is_active, False) + @skip("cannot activate a domain without mock registry") def test_get_status(self): """Returns proper status based on `is_active`.""" domain = Domain.objects.create(name="igorville.gov") diff --git a/src/run.sh b/src/run.sh index 487c54591..2d9391b93 100755 --- a/src/run.sh +++ b/src/run.sh @@ -6,4 +6,4 @@ set -o pipefail # Make sure that django's `collectstatic` has been run locally before pushing up to any environment, # so that the styles and static assets to show up correctly on any environment. -gunicorn registrar.config.wsgi -t 60 +gunicorn registrar.config.wsgi -t 60 --preload From 78aae0b7b522b0812a626b07bc7f9c64ab3b5735 Mon Sep 17 00:00:00 2001 From: Seamus Johnston Date: Fri, 21 Apr 2023 11:01:13 -0500 Subject: [PATCH 2/5] Fix GH pipeline --- .github/workflows/security-check.yaml | 6 +++++ .../runbooks/rotate_application_secrets.md | 3 +++ src/docker-compose.yml | 22 ++++++++++--------- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/.github/workflows/security-check.yaml b/.github/workflows/security-check.yaml index bd41e7966..01cd9c71d 100644 --- a/.github/workflows/security-check.yaml +++ b/.github/workflows/security-check.yaml @@ -24,6 +24,12 @@ jobs: DJANGO_SECRET_KEY: not-a-secret-jw7kQcb35fcDRIKp7K4fqZBmVvb+Sy4nkAGf44DxHi6EJl DATABASE_URL: "postgres://not_a_user:not_a_password@not_a_host" DJANGO_BASE_URL: "https://not_a_host" + REGISTRY_CL_ID: nothing + REGISTRY_PASSWORD: nothing + REGISTRY_CERT: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNMekNDQWRXZ0F3SUJBZ0lVWThXWEljcFVlVUk0TVUrU3NWbkIrOGErOUlnd0NnWUlLb1pJemowRUF3SXcKYlRFTE1Ba0dBMVVFQmhNQ1ZWTXhDekFKQmdOVkJBZ01Ba1JETVJNd0VRWURWUVFIREFwWFlYTm9hVzVuZEc5dQpNUXd3Q2dZRFZRUUtEQU5IVTBFeEREQUtCZ05WQkFzTUF6RTRSakVnTUI0R0ExVUVBd3dYUjA5V0lGQnliM1J2CmRIbHdaU0JTWldkcGMzUnlZWEl3SGhjTk1qTXdOREl4TVRVMU5ETTFXaGNOTWpRd05ESXdNVFUxTkRNMVdqQnQKTVFzd0NRWURWUVFHRXdKVlV6RUxNQWtHQTFVRUNBd0NSRU14RXpBUkJnTlZCQWNNQ2xkaGMyaHBibWQwYjI0eApEREFLQmdOVkJBb01BMGRUUVRFTU1Bb0dBMVVFQ3d3RE1UaEdNU0F3SGdZRFZRUUREQmRIVDFZZ1VISnZkRzkwCmVYQmxJRkpsWjJsemRISmhjakJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCRkN4bGVsN1ZoWHkKb1ZIRWY2N3FKamo5UDk0ZWdqdXNtSWVaNFRLYkxkM3RRRVgzZnFKdVk4WmZzWWN4N0s1K0NEdnJLMnZRdjlMYgpmamhMTjZad3FqK2pVekJSTUIwR0ExVWREZ1FXQkJRUEZCRHdnSlhOUXE4a1V0K1hyYzFFWm9wbW9UQWZCZ05WCkhTTUVHREFXZ0JRUEZCRHdnSlhOUXE4a1V0K1hyYzFFWm9wbW9UQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01Bb0cKQ0NxR1NNNDlCQU1DQTBnQU1FVUNJRVJEaml0VGR0UTB3eVNXb1hEbCtYbUpVUmdENUo0VHVudkFGeDlDSitCUwpBaUVBME42eTJoeGdFWkYxRXJGYW1VQW5EUHlQSFlJeFNJQkwwNW5ibE9IZFVLRT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + REGISTRY_KEY: LS0tLS1CRUdJTiBFTkNSWVBURUQgUFJJVkFURSBLRVktLS0tLQpNSUhzTUZjR0NTcUdTSWIzRFFFRkRUQktNQ2tHQ1NxR1NJYjNEUUVGRERBY0JBakJYK1UvdUFkQ3hBSUNDQUF3CkRBWUlLb1pJaHZjTkFna0ZBREFkQmdsZ2hrZ0JaUU1FQVNvRUVHWTNnblRGZ3F0UE5sVU93a2hvSHFrRWdaQlAKMG5FMWpSRXliTHBDNHFtaGczRXdaR2lXZDFWV2RLVEtyNXF3d3hsdjhCbHB1UHhtRGN4dTA1U3VReWhMcU5hWgpVNjRoZlFyYy94cnRnT3Mwc0ZXenlhY0hEaFhiQUdTQjdTTjc2WG55NU9wWDVZVGtRTFMvRTk4YmxFY3NQUWVuCkNqNTJnQzVPZ0JtYzl1cjZlbWY2bjd6TE5vUWovSzk4MEdIWjg5OVZHQ1J3OHhGZGIyb3IyU3dMcDd0V1Ixcz0KLS0tLS1FTkQgRU5DUllQVEVEIFBSSVZBVEUgS0VZLS0tLS0K + REGISTRY_KEY_PASSPHRASE: fake + REGISTRY_HOSTNAME: localhost steps: - name: Check out diff --git a/docs/operations/runbooks/rotate_application_secrets.md b/docs/operations/runbooks/rotate_application_secrets.md index 4ce146ea8..a4ccef43c 100644 --- a/docs/operations/runbooks/rotate_application_secrets.md +++ b/docs/operations/runbooks/rotate_application_secrets.md @@ -88,6 +88,9 @@ openssl req -new -x509 -days 365 \ ``` +(Hint: +`docker run --platform=linux/amd64 -it --rm -v $(pwd):/apps -w /apps alpine/openssl`.) + Encode them using: ```bash diff --git a/src/docker-compose.yml b/src/docker-compose.yml index e2b080716..636243617 100644 --- a/src/docker-compose.yml +++ b/src/docker-compose.yml @@ -27,22 +27,24 @@ services: - DJANGO_DEBUG=True # Tell Django where it is being hosted - DJANGO_BASE_URL=http://localhost:8080 + # Set a username for accessing the registry + - REGISTRY_CL_ID=nothing + # Set a password for accessing the registry + - REGISTRY_PASSWORD=nothing + # Set a private certifcate for accessing the registry + - REGISTRY_CERT=LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNMekNDQWRXZ0F3SUJBZ0lVWThXWEljcFVlVUk0TVUrU3NWbkIrOGErOUlnd0NnWUlLb1pJemowRUF3SXcKYlRFTE1Ba0dBMVVFQmhNQ1ZWTXhDekFKQmdOVkJBZ01Ba1JETVJNd0VRWURWUVFIREFwWFlYTm9hVzVuZEc5dQpNUXd3Q2dZRFZRUUtEQU5IVTBFeEREQUtCZ05WQkFzTUF6RTRSakVnTUI0R0ExVUVBd3dYUjA5V0lGQnliM1J2CmRIbHdaU0JTWldkcGMzUnlZWEl3SGhjTk1qTXdOREl4TVRVMU5ETTFXaGNOTWpRd05ESXdNVFUxTkRNMVdqQnQKTVFzd0NRWURWUVFHRXdKVlV6RUxNQWtHQTFVRUNBd0NSRU14RXpBUkJnTlZCQWNNQ2xkaGMyaHBibWQwYjI0eApEREFLQmdOVkJBb01BMGRUUVRFTU1Bb0dBMVVFQ3d3RE1UaEdNU0F3SGdZRFZRUUREQmRIVDFZZ1VISnZkRzkwCmVYQmxJRkpsWjJsemRISmhjakJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCRkN4bGVsN1ZoWHkKb1ZIRWY2N3FKamo5UDk0ZWdqdXNtSWVaNFRLYkxkM3RRRVgzZnFKdVk4WmZzWWN4N0s1K0NEdnJLMnZRdjlMYgpmamhMTjZad3FqK2pVekJSTUIwR0ExVWREZ1FXQkJRUEZCRHdnSlhOUXE4a1V0K1hyYzFFWm9wbW9UQWZCZ05WCkhTTUVHREFXZ0JRUEZCRHdnSlhOUXE4a1V0K1hyYzFFWm9wbW9UQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01Bb0cKQ0NxR1NNNDlCQU1DQTBnQU1FVUNJRVJEaml0VGR0UTB3eVNXb1hEbCtYbUpVUmdENUo0VHVudkFGeDlDSitCUwpBaUVBME42eTJoeGdFWkYxRXJGYW1VQW5EUHlQSFlJeFNJQkwwNW5ibE9IZFVLRT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + # Set a private certifcate's key for accessing the registry + - REGISTRY_KEY=LS0tLS1CRUdJTiBFTkNSWVBURUQgUFJJVkFURSBLRVktLS0tLQpNSUhzTUZjR0NTcUdTSWIzRFFFRkRUQktNQ2tHQ1NxR1NJYjNEUUVGRERBY0JBakJYK1UvdUFkQ3hBSUNDQUF3CkRBWUlLb1pJaHZjTkFna0ZBREFkQmdsZ2hrZ0JaUU1FQVNvRUVHWTNnblRGZ3F0UE5sVU93a2hvSHFrRWdaQlAKMG5FMWpSRXliTHBDNHFtaGczRXdaR2lXZDFWV2RLVEtyNXF3d3hsdjhCbHB1UHhtRGN4dTA1U3VReWhMcU5hWgpVNjRoZlFyYy94cnRnT3Mwc0ZXenlhY0hEaFhiQUdTQjdTTjc2WG55NU9wWDVZVGtRTFMvRTk4YmxFY3NQUWVuCkNqNTJnQzVPZ0JtYzl1cjZlbWY2bjd6TE5vUWovSzk4MEdIWjg5OVZHQ1J3OHhGZGIyb3IyU3dMcDd0V1Ixcz0KLS0tLS1FTkQgRU5DUllQVEVEIFBSSVZBVEUgS0VZLS0tLS0K + # set a passphrase for decrypting the registry key + - REGISTRY_KEY_PASSPHRASE=fake + # Set a URI for accessing the registry + - REGISTRY_HOSTNAME=localhost # --- These keys are obtained from `.env` file --- # Set a private JWT signing key for Login.gov - DJANGO_SECRET_LOGIN_KEY # AWS credentials - AWS_ACCESS_KEY_ID - AWS_SECRET_ACCESS_KEY - # Set a username for accessing the registry - - REGISTRY_CL_ID - # Set a password for accessing the registry - - REGISTRY_PASSWORD - # Set a private certifcate for accessing the registry - - REGISTRY_CERT - # Set a private certifcate's key for accessing the registry - - REGISTRY_KEY - # Set a URI for accessing the registry - - REGISTRY_HOSTNAME stdin_open: true tty: true ports: From ff8b89fa137ca5f7aa8f7698dfe6eb6683aaabc0 Mon Sep 17 00:00:00 2001 From: Seamus Johnston Date: Fri, 21 Apr 2023 11:22:30 -0500 Subject: [PATCH 3/5] Fix more test failures --- src/epplibwrapper/__init__.py | 5 ++++- src/mypy.ini | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/epplibwrapper/__init__.py b/src/epplibwrapper/__init__.py index 29061da21..41cb0448d 100644 --- a/src/epplibwrapper/__init__.py +++ b/src/epplibwrapper/__init__.py @@ -40,7 +40,10 @@ except NameError: pass # Attn: these imports should NOT be at the top of the file -from .client import CLIENT, commands +try: + from .client import CLIENT, commands +except ImportError: + pass __all__ = [ "CLIENT", diff --git a/src/mypy.ini b/src/mypy.ini index a151424a3..e8708cb67 100644 --- a/src/mypy.ini +++ b/src/mypy.ini @@ -7,6 +7,8 @@ strict_optional = True # implicit_optional: treat arguments a None default value as implicitly Optional? # `var: int = None` is equal to `var: Optional[int] = None` implicit_optional = True +# we'd still like mypy to succeed when 3rd party libraries are not installed +ignore_missing_imports = True [mypy.plugins.django-stubs] django_settings_module = "registrar.config.settings" From e386a039b245d678c5edc2e336c53a2240ef529c Mon Sep 17 00:00:00 2001 From: Seamus Johnston Date: Fri, 21 Apr 2023 11:25:50 -0500 Subject: [PATCH 4/5] Fix missing directory (unneeded) --- src/registrar/config/settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 6da8a1c83..43c4139bc 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -167,7 +167,6 @@ STATIC_ROOT = BASE_DIR / "registrar" / "public" STATICFILES_DIRS = [ BASE_DIR / "registrar" / "assets", - BASE_DIR / "epplibwrapper" / "assets", ] TEMPLATES = [ From 39c54fa79fc014523babe2f1ecd621571f9805ad Mon Sep 17 00:00:00 2001 From: Seamus Johnston Date: Fri, 28 Apr 2023 16:02:30 -0500 Subject: [PATCH 5/5] Respond to PR feedback --- .../runbooks/rotate_application_secrets.md | 3 +- src/epplibwrapper/__init__.py | 1 + src/epplibwrapper/client.py | 31 ++++++++++--------- src/registrar/models/domain.py | 2 -- src/run.sh | 2 +- 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/docs/operations/runbooks/rotate_application_secrets.md b/docs/operations/runbooks/rotate_application_secrets.md index a4ccef43c..456baf61d 100644 --- a/docs/operations/runbooks/rotate_application_secrets.md +++ b/docs/operations/runbooks/rotate_application_secrets.md @@ -88,8 +88,7 @@ openssl req -new -x509 -days 365 \ ``` -(Hint: -`docker run --platform=linux/amd64 -it --rm -v $(pwd):/apps -w /apps alpine/openssl`.) +(If you can't use openssl on your computer directly, you can access it using Docker as `docker run --platform=linux/amd64 -it --rm -v $(pwd):/apps -w /apps alpine/openssl`.) Encode them using: diff --git a/src/epplibwrapper/__init__.py b/src/epplibwrapper/__init__.py index 41cb0448d..ab3b63080 100644 --- a/src/epplibwrapper/__init__.py +++ b/src/epplibwrapper/__init__.py @@ -4,6 +4,7 @@ from types import SimpleNamespace try: from epplib import constants except ImportError: + # allow epplibwrapper to load without epplib, for testing and development pass logger = logging.getLogger(__name__) diff --git a/src/epplibwrapper/client.py b/src/epplibwrapper/client.py index d7f7068ca..7ddf0a03e 100644 --- a/src/epplibwrapper/client.py +++ b/src/epplibwrapper/client.py @@ -1,3 +1,5 @@ +"""Provide a wrapper around epplib to handle authentication and errors.""" + import logging from time import sleep @@ -21,12 +23,12 @@ try: # Write cert and key to disk CERT = Cert() KEY = Key() -except Exception as err: +except Exception: CERT = None # type: ignore KEY = None # type: ignore - logger.warning(err) logger.warning( - "Problem with client certificate. Registrar cannot contact registry." + "Problem with client certificate. Registrar cannot contact registry.", + exc_info=True, ) @@ -68,31 +70,31 @@ class EPPLibWrapper: with self._connect as wire: response = wire.send(command) except (ValueError, ParsingError) as err: - logger.debug(err) logger.warning( "%s failed to execute due to some syntax error." - % command.__class__.__name__ + % command.__class__.__name__, + exc_info=True, ) raise RegistryError() from err except TransportError as err: - logger.debug(err) logger.warning( "%s failed to execute due to a connection error." - % command.__class__.__name__ + % command.__class__.__name__, + exc_info=True, ) raise RegistryError() from err except LoginError as err: - logger.debug(err) logger.warning( "%s failed to execute due to a registry login error." - % command.__class__.__name__ + % command.__class__.__name__, + exc_info=True, ) raise RegistryError() from err except Exception as err: - logger.debug(err) logger.warning( "%s failed to execute due to an unknown error." - % command.__class__.__name__ + % command.__class__.__name__, + exc_info=True, ) raise RegistryError() from err else: @@ -119,7 +121,8 @@ try: # Initialize epplib CLIENT = EPPLibWrapper() logger.debug("registry client initialized") -except Exception as err: +except Exception: CLIENT = None # type: ignore - logger.warning(err) - logger.warning("Unable to configure epplib. Registrar cannot contact registry.") + logger.warning( + "Unable to configure epplib. Registrar cannot contact registry.", exc_info=True + ) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index b985c0629..dcf103586 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -7,8 +7,6 @@ from django.db import models from django_fsm import FSMField, transition # type: ignore from api.views import in_domains - -# from epplibwrapper import CLIENT as registry, commands from registrar.utility import errors from .utility.time_stamped_model import TimeStampedModel diff --git a/src/run.sh b/src/run.sh index 2d9391b93..487c54591 100755 --- a/src/run.sh +++ b/src/run.sh @@ -6,4 +6,4 @@ set -o pipefail # Make sure that django's `collectstatic` has been run locally before pushing up to any environment, # so that the styles and static assets to show up correctly on any environment. -gunicorn registrar.config.wsgi -t 60 --preload +gunicorn registrar.config.wsgi -t 60