diff --git a/src/Pipfile b/src/Pipfile index f60477a15..b3dc06e45 100644 --- a/src/Pipfile +++ b/src/Pipfile @@ -13,4 +13,5 @@ gunicorn = "*" psycopg2-binary = "*" [dev-packages] -django-debug-toolbar = "*" \ No newline at end of file +django-debug-toolbar = "*" +nplusone = "*" \ No newline at end of file diff --git a/src/Pipfile.lock b/src/Pipfile.lock index ca3732b96..ea5ec6fd5 100644 --- a/src/Pipfile.lock +++ b/src/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "3c3bdeb59b98dcd8c01350c36619ab12b593f8c25846dc3425f481b587fc0ae8" + "sha256": "9a25a3e7f6574f0253c8f94b397b651448658dc9ac3ffb7faa9ac1f5e89d8dba" }, "pipfile-spec": 6, "requires": {}, @@ -103,11 +103,11 @@ }, "marshmallow": { "hashes": [ - "sha256:00040ab5ea0c608e8787137627a8efae97fabd60552a05dc889c888f814e75eb", - "sha256:635fb65a3285a31a30f276f30e958070f5214c7196202caa5c7ecf28f5274bc7" + "sha256:1172ce82765bf26c24a3f9299ed6dbeeca4d213f638eaa39a37772656d7ce408", + "sha256:48e2d88d4ab431ad5a17c25556d9da529ea6e966876f2a38d274082e270287f0" ], "markers": "python_version >= '3.7'", - "version": "==3.17.0" + "version": "==3.17.1" }, "orderedmultidict": { "hashes": [ @@ -204,11 +204,11 @@ }, "setuptools": { "hashes": [ - "sha256:7a2e7e95c3bf33f356b4c59aee7a6848585c4219dd3e941e43cc117888f210e4", - "sha256:c04a012ae3a1b2cc2aeed4893377b70ea61c6c143d0acceea16ec4b60de6e40d" + "sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82", + "sha256:7732871f4f7fa58fb6bdcaeadb0161b2bd046c85905dbaa066bdcbcc81953b57" ], "markers": "python_version >= '3.7'", - "version": "==65.0.1" + "version": "==65.3.0" }, "six": { "hashes": [ @@ -236,6 +236,14 @@ "markers": "python_version >= '3.7'", "version": "==3.5.2" }, + "blinker": { + "hashes": [ + "sha256:1eb563df6fdbc39eeddc177d953203f99f097e9bf0e2b8f9f3cf18b6ca425e36", + "sha256:923e5e2f69c155f2cc42dafbbd70e16e3fde24d2d4aa2ab72fbe386238892462" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.5" + }, "django": { "hashes": [ "sha256:031ccb717782f6af83a0063a1957686e87cb4581ea61b47b3e9addf60687989a", @@ -246,11 +254,27 @@ }, "django-debug-toolbar": { "hashes": [ - "sha256:89a52128309eb4da12738801ff0c202d2ff8730d1c3225fac6acf630c303e661", - "sha256:97965f2630692de316ea0c1ca5bfa81660d7ba13146dbc6be2059cf55b35d0e5" + "sha256:95fc2fd29c56cc86678aae9f6919ececefe892f2a78c4004b193a223a8380c3d", + "sha256:fe7fe3f21865218827e2162ecc06eba386dfe8cffe4f3501c49bb4359e06a0e6" ], "index": "pypi", - "version": "==3.5.0" + "version": "==3.6.0" + }, + "nplusone": { + "hashes": [ + "sha256:1726c0a10c0aa7eabb04e24db2882ff97b6b7ee29d729a8d97dcbd12ef5a5651", + "sha256:96b1e6e29e6af3e71b67d0cc012a5ec8c97c6a2f5399f4ba41a2bbe0e253a9ac" + ], + "index": "pypi", + "version": "==1.0.0" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" }, "sqlparse": { "hashes": [ diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 7208dc12f..449535ac7 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -1,8 +1,6 @@ """ Django settings for .gov registrar project. -Generated by 'django-admin startproject' using Django 4.0.6. - For more information on this file, see https://docs.djangoproject.com/en/4.0/topics/settings/ @@ -19,70 +17,144 @@ $ docker-compose exec app python manage.py shell """ import environs -import os from cfenv import AppEnv from pathlib import Path +### ### +# Setup code goes here # +### ### + env = environs.Env() # Get secrets from Cloud.gov user provided service, if exists # If not, get secrets from environment variables -key_service = AppEnv().get_service(name="getgov-credentials") +key_service = AppEnv().get_service(name='getgov-credentials') if key_service and key_service.credentials: secret = key_service.credentials.get else: secret = env -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent +### ### +# Values obtained externally # +### ### -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = secret("DJANGO_SECRET_KEY") +path = Path(__file__) + +env_db_url = env.dj_db_url("DATABASE_URL") +env_debug = env.bool("DJANGO_DEBUG", default=False) +env_log_level = env.str("DJANGO_LOG_LEVEL", "DEBUG") + +secret_key = secret("DJANGO_SECRET_KEY") + +# region: Basic Django Config-----------------------------------------------### + +# Build paths inside the project like this: BASE_DIR / "subdir". +BASE_DIR = path.resolve().parent.parent # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = env.bool("DJANGO_DEBUG", default=False) - -# TODO: configure and document security settings -ALLOWED_HOSTS = ["getgov-unstable.app.cloud.gov", "get.gov"] -ALLOWED_CIDR_NETS = ["10.0.0.0/8"] # nosec -USE_X_FORWARDED_HOST = True -SESSION_COOKIE_SECURE = True -SESSION_COOKIE_HTTPONLY = True -CSRF_COOKIE_SECURE = True -CSRF_COOKIE_HTTPONLY = True -CORS_ALLOW_ALL_ORIGINS = False +DEBUG = env_debug -# TODO: are all of these needed? Need others? +# Applications are modular pieces of code. +# They are provided by Django, by third-parties, or by yourself. +# Installing them here makes them available for execution. +# Do not access INSTALLED_APPS directly. Use `django.apps.apps` instead. INSTALLED_APPS = [ + # Django automatic admin interface reads metadata + # from database models to provide a quick, model-centric + # interface where trusted users can manage content "django.contrib.admin", + + # vv Required by django.contrib.admin vv + # the "user" model! *\o/* "django.contrib.auth", + # generic interface for Django models "django.contrib.contenttypes", + # required for CSRF protection and many other things "django.contrib.sessions", + # framework for displaying messages to the user "django.contrib.messages", + # ^^ Required by django.contrib.admin ^^ + + # collects static files from each of your applications + # (and any other places you specify) into a single location + # that can easily be served in production "django.contrib.staticfiles", ] -# TODO: document these for future maintainers +# Middleware are routines for processing web requests. +# Adding them here turns them "on"; Django will perform the +# specified routines on each incoming request and outgoing response. MIDDLEWARE = [ + # django-allow-cidr: enable use of CIDR IP ranges in ALLOWED_HOSTS "allow_cidr.middleware.AllowCIDRMiddleware", + + # provide security enhancements to the request/response cycle "django.middleware.security.SecurityMiddleware", + + # store and retrieve arbitrary data on a per-site-visitor basis "django.contrib.sessions.middleware.SessionMiddleware", + + # add a few conveniences for perfectionists, see documentation "django.middleware.common.CommonMiddleware", + + # add protection against Cross Site Request Forgeries by adding + # hidden form fields to POST forms and checking requests for the correct value "django.middleware.csrf.CsrfViewMiddleware", + + # add `user` (the currently-logged-in user) to incoming HttpRequest objects "django.contrib.auth.middleware.AuthenticationMiddleware", + + # provide framework for displaying messages to the user, see documentation "django.contrib.messages.middleware.MessageMiddleware", + + # provide clickjacking protection via the X-Frame-Options header "django.middleware.clickjacking.XFrameOptionsMiddleware", + + # django-csp: enable use of Content-Security-Policy header "csp.middleware.CSPMiddleware", ] +# application object used by Django’s built-in servers (e.g. `runserver`) +WSGI_APPLICATION = "registrar.config.wsgi.application" + +#endregion +# region: Assets and HTML and Caching---------------------------------------### + +# https://docs.djangoproject.com/en/4.0/howto/static-files/ + + +# Caching is disabled by default. +# For a low to medium traffic site, caching causes more +# problems than it solves. Should caching be desired, +# a reasonable start might be: +# CACHES = { +# "default": { +# "BACKEND": "django.core.cache.backends.db.DatabaseCache", +# } +# } + +# Absolute path to the directory where `collectstatic` +# will place static files for deployment. +# Do not use this directory for permanent storage - +# it is for Django! +STATIC_ROOT = BASE_DIR / "static" + # TODO: decide on template engine and document in ADR TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [BASE_DIR / "templates"], + # look for templates inside installed apps + # required by django-debug-toolbar "APP_DIRS": True, "OPTIONS": { + # IMPORTANT security setting: escapes HTMLEntities, + # helping to prevent XSS attacks + "autoescape": True, + # context processors are callables which return + # dicts - Django merges them into the context + # dictionary used to render the templates "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", @@ -93,15 +165,116 @@ TEMPLATES = [ }, ] +#endregion +# region: Database----------------------------------------------------------### + +# Wrap each view in a transaction on the database +# A decorator can be used for views which have no database activity: +# from django.db import transaction +# @transaction.non_atomic_requests +env_db_url["ATOMIC_REQUESTS"] = True + +DATABASES = { + # dj-database-url package takes the supplied Postgres connection string + # and converts it into a dictionary with the correct USER, HOST, etc + "default": env_db_url, +} + +# Specify default field type to use for primary keys +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +#endregion +# region: Email-------------------------------------------------------------### + +# email address to use for various automated correspondence +# TODO: pick something sensible here +DEFAULT_FROM_EMAIL = "registrar@get.gov" + +# connect to an (external) SMTP server for sending email +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" + +# TODO: configure these when the values are known +# EMAIL_HOST = "" +# EMAIL_HOST_PASSWORD = "" +# EMAIL_HOST_USER = "" +# EMAIL_PORT = 587 + +# for mail sent with mail_admins or mail_managers +EMAIL_SUBJECT_PREFIX = "[Attn: .gov admin] " + +# use a TLS (secure) connection when talking to the SMTP server +# TLS generally uses port 587 +EMAIL_USE_TLS = True + +# mutually exclusive with EMAIL_USE_TLS = True +# SSL generally uses port 465 +EMAIL_USE_SSL = False + +# timeout in seconds for blocking operations, like the connection attempt +EMAIL_TIMEOUT = 30 + +# email address to use for sending error reports +SERVER_EMAIL = "root@get.gov" + +#endregion +# region: Headers-----------------------------------------------------------### + +# Content-Length header is set by django.middleware.common.CommonMiddleware + +# X-Frame-Options header is set by django.middleware.clickjacking.XFrameOptionsMiddleware +# and configured in the Security and Privacy section of this file. +# Strict-Transport-Security is set by django.middleware.security.SecurityMiddleware +# and configured in the Security and Privacy section of this file. + +# prefer contents of X-Forwarded-Host header to Host header +# as Host header may contain a proxy rather than the actual client +USE_X_FORWARDED_HOST = True + +#endregion +# region: Internationalisation----------------------------------------------### + +# https://docs.djangoproject.com/en/4.0/topics/i18n/ + +# Charset to use for HttpResponse objects; used in Content-Type header +DEFAULT_CHARSET = "utf-8" + +# provide fallback language if translation file is missing or +# user's locale is not supported - requires USE_I18N = True +LANGUAGE_CODE = "en-us" + +# allows language cookie to be sent if the user +# is coming to our site from an external page. +LANGUAGE_COOKIE_SAMESITE = None + +# only send via HTTPS connection +LANGUAGE_COOKIE_SECURE = True + +# to display datetimes in templates +# and to interpret datetimes entered in forms +TIME_ZONE = "UTC" + +# enable Django’s translation system +USE_I18N = True + +# enable localized formatting of numbers and dates +USE_L10N = True + +# make datetimes timezone-aware by default +USE_TZ = True + +#endregion +# region: Logging-----------------------------------------------------------### + # No file logger is configured, because containerized apps # do not log to the file system. +# TODO: Configure better logging options LOGGING = { "version": 1, "disable_existing_loggers": False, "formatters": { "verbose": { "format": "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] " - "%(message)s", # noqa + "%(message)s", "datefmt": "%d/%b/%Y %H:%M:%S", }, "simple": { @@ -110,7 +283,7 @@ LOGGING = { }, "handlers": { "console": { - "level": "DEBUG" if DEBUG else "INFO", + "level": "INFO", "class": "logging.StreamHandler", "formatter": "verbose", }, @@ -119,7 +292,7 @@ LOGGING = { "django": { "handlers": ["console"], "propagate": True, - "level": os.getenv("DJANGO_LOG_LEVEL", "DEBUG"), + "level": env_log_level, }, "django.template": { "handlers": ["console"], @@ -134,67 +307,208 @@ LOGGING = { }, } -# Database -# https://docs.djangoproject.com/en/4.0/ref/settings/#databases - -DATABASES = { - "default": env.dj_db_url("DATABASE_URL"), -} - - -# Internationalization -# https://docs.djangoproject.com/en/4.0/topics/i18n/ - -LANGUAGE_CODE = "en-us" -TIME_ZONE = "UTC" -USE_I18N = True -USE_TZ = True -USE_L10N = True - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/4.0/howto/static-files/ - -STATIC_ROOT = BASE_DIR / "static" - -STATIC_URL = "static/" -ADMIN_URL = "admin/" - -ROOT_URLCONF = "registrar.config.urls" -WSGI_APPLICATION = "registrar.config.wsgi.application" - -# TODO: FAC example for REST framework -API_VERSION = "0" -REST_FRAMEWORK = { - "DEFAULT_AUTHENTICATION_CLASSES": [ - "rest_framework.authentication.BasicAuthentication", - "users.auth.ExpiringTokenAuthentication", - ], - "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), - "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", - "PAGE_SIZE": 10, - "TEST_REQUEST_RENDERER_CLASSES": [ - "rest_framework.renderers.MultiPartRenderer", - "rest_framework.renderers.JSONRenderer", - "rest_framework.renderers.TemplateHTMLRenderer", - "rest_framework.renderers.BrowsableAPIRenderer", - ], - "TEST_REQUEST_DEFAULT_FORMAT": "api", -} +#endregion +# region: Login-------------------------------------------------------------### # TODO: FAC example for login.gov -SIMPLE_JWT = { - "ALGORITHM": "RS256", - "AUDIENCE": None, - "ISSUER": "https://idp.int.identitysandbox.gov/", - "JWK_URL": "https://idp.int.identitysandbox.gov/api/openid_connect/certs", - "LEEWAY": 0, - "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.UntypedToken",), - "USER_ID_CLAIM": "sub", -} -TOKEN_AUTH = {"TOKEN_TTL": 3600} +# SIMPLE_JWT = { +# "ALGORITHM": "RS256", +# "AUDIENCE": None, +# "ISSUER": "https://idp.int.identitysandbox.gov/", +# "JWK_URL": "https://idp.int.identitysandbox.gov/api/openid_connect/certs", +# "LEEWAY": 0, +# "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.UntypedToken",), +# "USER_ID_CLAIM": "sub", +# } +# TOKEN_AUTH = {"TOKEN_TTL": 3600} + +#endregion +# region: Rest Framework/API------------------------------------------------### + +# Enable CORS if api is served at subdomain +# https://github.com/adamchainz/django-cors-headers +# TODO: FAC example for REST framework +# API_VERSION = "0" +# REST_FRAMEWORK = { +# "DEFAULT_AUTHENTICATION_CLASSES": [ +# "rest_framework.authentication.BasicAuthentication", +# "users.auth.ExpiringTokenAuthentication", +# ], +# "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), +# "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", +# "PAGE_SIZE": 10, +# "TEST_REQUEST_RENDERER_CLASSES": [ +# "rest_framework.renderers.MultiPartRenderer", +# "rest_framework.renderers.JSONRenderer", +# "rest_framework.renderers.TemplateHTMLRenderer", +# "rest_framework.renderers.BrowsableAPIRenderer", +# ], +# "TEST_REQUEST_DEFAULT_FORMAT": "api", +# } + +#endregion +# region: Routing-----------------------------------------------------------### + +## Set by django.middleware.common.CommonMiddleware +# APPEND_SLASH = True +# PREPEND_WWW = False + +# full Python import path to the root URLconf +ROOT_URLCONF = "registrar.config.urls" + +# URL to use when referring to static files located in STATIC_ROOT +# Must be relative and end with "/" +STATIC_URL = "public/" + +#endregion +# region: Security and Privacy----------------------------------------------### + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = secret_key + +# Use this variable for doing SECRET_KEY rotation, see documentation +SECRET_KEY_FALLBACKS = [] + +## Set by django.middleware.security.SecurityMiddleware +# SECURE_CONTENT_TYPE_NOSNIFF = True +# SECURE_CROSS_ORIGIN_OPENER_POLICY = "same-origin" +# SECURE_REDIRECT_EXEMPT = [] +# SECURE_REFERRER_POLICY = "same-origin" +# SECURE_SSL_HOST = None + +## Overridden from django.middleware.security.SecurityMiddleware +# adds the includeSubDomains directive to the HTTP Strict Transport Security header +SECURE_HSTS_INCLUDE_SUBDOMAINS = True +# adds the preload directive to the HTTP Strict Transport Security header +SECURE_HSTS_PRELOAD = True +# TODO: set this value to 31536000 (1 year) for production +SECURE_HSTS_SECONDS = 300 +# redirect all non-HTTPS requests to HTTPS +SECURE_SSL_REDIRECT = True + +## Set by django.middleware.common.CommonMiddleware +# DISALLOWED_USER_AGENTS = [] + +# The host/domain names that Django can serve. +# This is a security measure to prevent HTTP Host header attacks, +# which are possible even under many seemingly-safe +# web server configurations. +ALLOWED_HOSTS = [ + "getgov-unstable.app.cloud.gov", + "get.gov", +] -# Default primary key field type -# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field +# Extend ALLOWED_HOSTS. +# IP addresses can also be hosts, which are used by internal +# load balancers for health checks, etc. +ALLOWED_CIDR_NETS = ["10.0.0.0/8"] -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +## Below are some protections from cross-site request forgery. +# This is canonically done by including a nonce value +# in pages sent to the user, which the user is expected +# to send back. The specifics of implementation are +# intricate and varied. + +# Store the token server-side, do not send it +# to the user via a cookie. This means each page +# which requires protection must place the token +# in the HTML explicitly, otherwise the user will +# get a 403 error when they submit. +CSRF_USE_SESSIONS = True + +# Expiry of CSRF cookie, in seconds. +# None means "use session-based CSRF cookies". +CSRF_COOKIE_AGE = None + +# Prevent JavaScript from reading the CSRF cookie. +# Has no effect with CSRF_USE_SESSIONS = True. +CSRF_COOKIE_HTTPONLY = True + +# Only send the cookie via HTTPS connections. +# Has no effect with CSRF_USE_SESSIONS = True. +CSRF_COOKIE_SECURE = True + +# Protect from non-targeted attacks by obscuring +# the CSRF cookie name from the default. +# Has no effect with CSRF_USE_SESSIONS = True. +CSRF_COOKIE_NAME = "CrSiReFo" + +# Prevents CSRF cookie from being sent if the user +# is coming to our site from an external page. +# Has no effect with CSRF_USE_SESSIONS = True. +CSRF_COOKIE_SAMESITE = "Strict" + +# Change header name to match cookie name. +# Has no effect with CSRF_USE_SESSIONS = True. +CSRF_HEADER_NAME = "HTTP_X_CRSIREFO" + +# Max parameters that may be received via GET or POST +# TODO: 1000 is the default, may need to tune upward for +# large DNS zone files, if records are represented by +# individual form fields. +DATA_UPLOAD_MAX_NUMBER_FIELDS = 1000 + +# age of session cookies, in seconds (28800 = 8 hours) +SESSION_COOKIE_AGE = 28800 + +# instruct the browser to forbid client-side JavaScript +# from accessing the cookie +SESSION_COOKIE_HTTPONLY = True + +# are we a spring boot application? who knows! +SESSION_COOKIE_NAME = "JSESSIONID" + +# Prevents session cookie from being sent if the user +# is coming to our site from an external page. +SESSION_COOKIE_SAMESITE = "Strict" + +# instruct browser to only send cookie via HTTPS +SESSION_COOKIE_SECURE = True + +## Set by django.middleware.clickjacking.XFrameOptionsMiddleware +# prevent clickjacking by instructing the browser not to load +# our site within an iframe +# X_FRAME_OPTIONS = "Deny" + +#endregion +# region: Testing-----------------------------------------------------------### + +# Additional directories searched for fixture files. +# The fixtures directory of each application is searched by default. +# Must use unix style "/" path separators. +FIXTURE_DIRS = [] + +#endregion + + +### ### +# Development settings # +### ### + +if DEBUG: + # used by debug() context processor + INTERNAL_IPS = [ + "127.0.0.1", + "::1", + ] + + # allow dev laptop to connect + ALLOWED_HOSTS += ("localhost",) + SECURE_SSL_REDIRECT = False + SECURE_HSTS_PRELOAD = False + + # discover potentially inefficient database queries + # TODO: use settings overrides to ensure this always is True during tests + INSTALLED_APPS += ("nplusone.ext.django",) + MIDDLEWARE += ("nplusone.ext.django.NPlusOneMiddleware",) + NPLUSONE_RAISE = True + + # insert the amazing django-debug-toolbar + INSTALLED_APPS += ("debug_toolbar",) + MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") + + DEBUG_TOOLBAR_CONFIG = { + # due to Docker, bypass Debug Toolbar's check on INTERNAL_IPS + "SHOW_TOOLBAR_CALLBACK": lambda _: True, + }