Repository: cisagov/manage.get.gov

Issue:
Audit the Django settings file
Django has a [large number of settings](https://docs.djangoproject.com/en/4.1/ref/settings/) and some [best advice](https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/) for preparing for deployment.

We need to update the settings file to conform to best practices and ensure all settings are reasonably documented for future maintainers.


Discussion:

Resolving patch:
diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py
--- 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,10 +17,13 @@
 
 """
 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
@@ -33,56 +34,117 @@
 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__)
 
-# SECURITY WARNING: don't run with debug turned on in production!
-DEBUG = env.bool("DJANGO_DEBUG", default=False)
+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")
 
-# 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
+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_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 +155,117 @@
     },
 ]
 
+# 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 +274,7 @@
     },
     "handlers": {
         "console": {
-            "level": "DEBUG" if DEBUG else "INFO",
+            "level": "INFO",
             "class": "logging.StreamHandler",
             "formatter": "verbose",
         },
@@ -119,7 +283,7 @@
         "django": {
             "handlers": ["console"],
             "propagate": True,
-            "level": os.getenv("DJANGO_LOG_LEVEL", "DEBUG"),
+            "level": env_log_level,
         },
         "django.template": {
             "handlers": ["console"],
@@ -134,67 +298,208 @@
     },
 }
 
-# Database
-# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
+# endregion
+# region: Login-------------------------------------------------------------###
 
-DATABASES = {
-    "default": env.dj_db_url("DATABASE_URL"),
-}
+# 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}
 
+# endregion
+# region: Rest Framework/API------------------------------------------------###
 
-# Internationalization
-# https://docs.djangoproject.com/en/4.0/topics/i18n/
+# 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",
+# }
 
-LANGUAGE_CODE = "en-us"
-TIME_ZONE = "UTC"
-USE_I18N = True
-USE_TZ = True
-USE_L10N = True
+# endregion
+# region: Routing-----------------------------------------------------------###
 
-# Static files (CSS, JavaScript, Images)
-# https://docs.djangoproject.com/en/4.0/howto/static-files/
+# ~ Set by django.middleware.common.CommonMiddleware
+# APPEND_SLASH = True
+# PREPEND_WWW = False
 
-STATIC_ROOT = BASE_DIR / "static"
+# full Python import path to the root URLconf
+ROOT_URLCONF = "registrar.config.urls"
 
-STATIC_URL = "static/"
-ADMIN_URL = "admin/"
+# URL to use when referring to static files located in STATIC_ROOT
+# Must be relative and end with "/"
+STATIC_URL = "public/"
 
-ROOT_URLCONF = "registrar.config.urls"
-WSGI_APPLICATION = "registrar.config.wsgi.application"
+# endregion
+# region: Security and Privacy----------------------------------------------###
 
-# 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",
-}
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = secret_key
 
-# 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}
+# Use this variable for doing SECRET_KEY rotation, see documentation
+SECRET_KEY_FALLBACKS: "list[str]" = []
 
+# ~ 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
 
-# Default primary key field type
-# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
+# ~ 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
 
-DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
+# ~ 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",
+]
+
+
+# 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"]
+
+# ~ 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: "list[str]" = []
+
+# 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,
+    }


Test patch: