diff --git a/src/objecttypes/conf/base.py b/src/objecttypes/conf/base.py index 2192d521..11a4a3ae 100644 --- a/src/objecttypes/conf/base.py +++ b/src/objecttypes/conf/base.py @@ -1,90 +1,18 @@ -import os - -from django.urls import reverse_lazy - -from sentry_sdk.integrations import django, redis +from open_api_framework.conf.base import * # noqa +from open_api_framework.conf.utils import config from .api import * # noqa -from .utils import config - -try: - from sentry_sdk.integrations import celery -except Exception: # no celery in this project - celery = None - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -DJANGO_PROJECT_DIR = os.path.abspath( - os.path.join(os.path.dirname(__file__), os.path.pardir) -) -BASE_DIR = os.path.abspath( - os.path.join(DJANGO_PROJECT_DIR, os.path.pardir, os.path.pardir) -) # # Core Django settings # -SITE_ID = config("SITE_ID", 1) - -DEFAULT_AUTO_FIELD = "django.db.models.AutoField" - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = config("SECRET_KEY") - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = config("DEBUG", default=False) - -ALLOWED_HOSTS = config("ALLOWED_HOSTS", default="", split=True) -IS_HTTPS = config("IS_HTTPS", default=not DEBUG) - -USE_X_FORWARDED_HOST = config("USE_X_FORWARDED_HOST", default=False) - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.postgresql", - "NAME": config("DB_NAME", "objecttypes"), - "USER": config("DB_USER", "objecttypes"), - "PASSWORD": config("DB_PASSWORD", "objecttypes"), - "HOST": config("DB_HOST", "localhost"), - "PORT": config("DB_PORT", 5432), - } -} # Application definition -INSTALLED_APPS = [ - # Note: contenttypes should be first, see Django ticket #10827 - "django.contrib.contenttypes", - "django.contrib.auth", - "django.contrib.sessions", - # Note: If enabled, at least one Site object is required - "django.contrib.sites", - "django.contrib.messages", - "django.contrib.staticfiles", - # django-admin-index - "ordered_model", - "django_admin_index", - # Optional applications. - "django.contrib.admin", - # 'django.contrib.admindocs', - # 'django.contrib.humanize', - # 'django.contrib.sitemaps', +INSTALLED_APPS = INSTALLED_APPS + [ # External applications. - "axes", "jsonsuit.apps.JSONSuitConfig", - "mozilla_django_oidc", - "mozilla_django_oidc_db", - "django_jsonform", - "rest_framework", - "solo", - "drf_spectacular", - "vng_api_common", - "django_setup_configuration", # Two-factor authentication in the Django admin, enforced. - "django_otp", - "django_otp.plugins.otp_static", - "django_otp.plugins.otp_totp", - "two_factor", - "maykin_2fa", "sharing_configs", # Project applications. "objecttypes.accounts", @@ -95,338 +23,33 @@ "objecttypes.utils", ] -MIDDLEWARE = [ - "django.middleware.security.SecurityMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - # 'django.middleware.locale.LocaleMiddleware', - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "maykin_2fa.middleware.OTPMiddleware", - "mozilla_django_oidc_db.middleware.SessionRefresh", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", - "axes.middleware.AxesMiddleware", -] - -ROOT_URLCONF = "objecttypes.urls" - -# List of callables that know how to import templates from various sources. -RAW_TEMPLATE_LOADERS = ( - "django.template.loaders.filesystem.Loader", - "django.template.loaders.app_directories.Loader", - # 'admin_tools.template_loaders.Loader', -) - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [ - os.path.join(DJANGO_PROJECT_DIR, "templates"), - ], - "APP_DIRS": False, # conflicts with explicity specifying the loaders - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - "objecttypes.utils.context_processors.settings", - ], - "loaders": RAW_TEMPLATE_LOADERS, - }, - }, -] - -WSGI_APPLICATION = "objecttypes.wsgi.application" - -# Database: Defined in target specific settings files. -# https://docs.djangoproject.com/en/3.0/ref/settings/#databases - -# Password validation -# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", - }, -] - # Internationalization # https://docs.djangoproject.com/en/3.0/topics/i18n/ -LANGUAGE_CODE = "en-us" - -TIME_ZONE = "Europe/Amsterdam" - -USE_I18N = True - -USE_TZ = True - -USE_THOUSAND_SEPARATOR = True - -# Translations -LOCALE_PATHS = (os.path.join(DJANGO_PROJECT_DIR, "conf", "locale"),) - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/3.0/howto/static-files/ - -STATIC_URL = "/static/" - -STATIC_ROOT = os.path.join(BASE_DIR, "static") - -# Additional locations of static files -STATICFILES_DIRS = (os.path.join(DJANGO_PROJECT_DIR, "static"),) - -# List of finder classes that know how to find static files in -# various locations. -STATICFILES_FINDERS = [ - "django.contrib.staticfiles.finders.FileSystemFinder", - "django.contrib.staticfiles.finders.AppDirectoriesFinder", - # 'django.contrib.staticfiles.finders.DefaultStorageFinder', -] - -MEDIA_ROOT = os.path.join(BASE_DIR, "media") -MEDIA_URL = "/media/" -FILE_UPLOAD_PERMISSIONS = 0o644 - -FIXTURE_DIRS = (os.path.join(DJANGO_PROJECT_DIR, "fixtures"),) - -LOGGING_DIR = os.path.join(BASE_DIR, "log") +LANGUAGE_CODE = "en-us" # FIXME should this be "nl-nl"? -LOGGING = { - "version": 1, - "disable_existing_loggers": False, - "formatters": { - "verbose": { - "format": "%(asctime)s %(levelname)s %(name)s %(module)s %(process)d %(thread)d %(message)s" - }, - "timestamped": {"format": "%(asctime)s %(levelname)s %(name)s %(message)s"}, - "simple": {"format": "%(levelname)s %(message)s"}, - "performance": { - "format": "%(asctime)s %(process)d | %(thread)d | %(message)s", - }, - }, - "filters": { - "require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}, - }, - "handlers": { - "mail_admins": { - "level": "ERROR", - "filters": ["require_debug_false"], - "class": "django.utils.log.AdminEmailHandler", - }, - "null": { - "level": "DEBUG", - "class": "logging.NullHandler", - }, - "console": { - "level": "DEBUG", - "class": "logging.StreamHandler", - "formatter": "timestamped", - }, - "django": { - "level": "DEBUG", - "class": "logging.handlers.RotatingFileHandler", - "filename": os.path.join(LOGGING_DIR, "django.log"), - "formatter": "verbose", - "maxBytes": 1024 * 1024 * 10, # 10 MB - "backupCount": 10, - }, - "project": { - "level": "DEBUG", - "class": "logging.handlers.RotatingFileHandler", - "filename": os.path.join(LOGGING_DIR, "objecttypes.log"), - "formatter": "verbose", - "maxBytes": 1024 * 1024 * 10, # 10 MB - "backupCount": 10, - }, - "performance": { - "level": "INFO", - "class": "logging.handlers.RotatingFileHandler", - "filename": os.path.join(LOGGING_DIR, "performance.log"), - "formatter": "performance", - "maxBytes": 1024 * 1024 * 10, # 10 MB - "backupCount": 10, - }, - }, - "loggers": { - "objecttypes": { - "handlers": ["project"], - "level": "INFO", - "propagate": True, - }, - "django.request": { - "handlers": ["django"], - "level": "ERROR", - "propagate": True, - }, - "django.template": { - "handlers": ["console"], - "level": "INFO", - "propagate": True, - }, - "mozilla_django_oidc": { - "handlers": ["project"], - "level": "DEBUG", - }, - }, -} - -# -# Additional Django settings -# - -# Custom user model -AUTH_USER_MODEL = "accounts.User" - -# Allow logging in with both username+password and email+password -AUTHENTICATION_BACKENDS = [ - "axes.backends.AxesBackend", - "objecttypes.accounts.backends.UserModelEmailBackend", - "django.contrib.auth.backends.ModelBackend", - "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend", -] - -SESSION_COOKIE_NAME = "objecttypes_sessionid" - -LOGIN_REDIRECT_URL = reverse_lazy("admin:index") -LOGOUT_REDIRECT_URL = reverse_lazy("admin:index") +TIME_ZONE = "Europe/Amsterdam" # FIXME should this be "UTC"? # # Custom settings # PROJECT_NAME = "Objecttypes" SITE_TITLE = "Starting point" -ENVIRONMENT = config("ENVIRONMENT", "") -SHOW_ALERT = True +SHOW_ALERT = True # FIXME this doesn't seem to be used anywhere? -# -# Library settings -# +############################## +# # +# 3RD PARTY LIBRARY SETTINGS # +# # +############################## # Django-Admin-Index -ADMIN_INDEX_SHOW_REMAINING_APPS_TO_SUPERUSERS = False ADMIN_INDEX_DISPLAY_DROP_DOWN_MENU_CONDITION_FUNCTION = ( "objecttypes.utils.admin_index.should_display_dropdown_menu" ) -# Django-Axes -# -# The number of login attempts allowed before a record is created for the -# failed logins. Default: 3 -AXES_FAILURE_LIMIT = 10 -# If set, defines a period of inactivity after which old failed login attempts -# will be forgotten. Can be set to a python timedelta object or an integer. If -# an integer, will be interpreted as a number of hours. Default: None -AXES_COOLOFF_TIME = 1 -# If set, specifies a template to render when a user is locked out. Template -# receives cooloff_time and failure_limit as context variables. Default: None -AXES_LOCKOUT_TEMPLATE = "account_blocked.html" -AXES_LOCKOUT_PARAMETERS = [["ip_address", "user_agent", "username"]] - -# The default meta precedence order -IPWARE_META_PRECEDENCE_ORDER = ( - "HTTP_X_FORWARDED_FOR", - "X_FORWARDED_FOR", # , , - "HTTP_CLIENT_IP", - "HTTP_X_REAL_IP", - "HTTP_X_FORWARDED", - "HTTP_X_CLUSTER_CLIENT_IP", - "HTTP_FORWARDED_FOR", - "HTTP_FORWARDED", - "HTTP_VIA", - "REMOTE_ADDR", -) - -# -# Sending EMAIL -# -EMAIL_HOST = config("EMAIL_HOST", default="localhost") -# disabled on Google Cloud, use 487 instead: -EMAIL_PORT = config("EMAIL_PORT", default=25) -EMAIL_HOST_USER = config("EMAIL_HOST_USER", default="") -EMAIL_HOST_PASSWORD = config("EMAIL_HOST_PASSWORD", default="") -EMAIL_USE_TLS = config("EMAIL_USE_TLS", default=False) -EMAIL_TIMEOUT = 10 - -DEFAULT_FROM_EMAIL = config("DEFAULT_FROM_EMAIL", "objecttypes@example.com") - -# Sentry SDK -SENTRY_DSN = config("SENTRY_DSN", None) - -SENTRY_SDK_INTEGRATIONS = [ - django.DjangoIntegration(), - redis.RedisIntegration(), -] -if celery is not None: - SENTRY_SDK_INTEGRATIONS.append(celery.CeleryIntegration()) - -if SENTRY_DSN: - import sentry_sdk - - SENTRY_CONFIG = { - "dsn": SENTRY_DSN, - "release": config("VERSION_TAG", "VERSION_TAG not set"), - "environment": ENVIRONMENT, - } - - sentry_sdk.init( - **SENTRY_CONFIG, integrations=SENTRY_SDK_INTEGRATIONS, send_default_pii=True - ) - -# -# Elastic APM -# -ELASTIC_APM_SERVER_URL = config("ELASTIC_APM_SERVER_URL", None) -ELASTIC_APM = { - "SERVICE_NAME": config("ELASTIC_APM_SERVICE_NAME", "Objecttypes API"), - "SECRET_TOKEN": config("ELASTIC_APM_SECRET_TOKEN", "default"), - "SERVER_URL": ELASTIC_APM_SERVER_URL, - "ENABLED": bool(ELASTIC_APM_SERVER_URL), -} -if ELASTIC_APM_SERVER_URL: - MIDDLEWARE = ["elasticapm.contrib.django.middleware.TracingMiddleware"] + MIDDLEWARE - INSTALLED_APPS = INSTALLED_APPS + [ - "elasticapm.contrib.django", - ] - - -# -# MAYKIN-2FA -# Uses django-two-factor-auth under the hood, so relevant upstream package settings -# apply too. -# - -# we run the admin site monkeypatch instead. -TWO_FACTOR_PATCH_ADMIN = False -# add entries from AUTHENTICATION_BACKENDS that already enforce their own two-factor -# auth, avoiding having some set up MFA again in the project. -MAYKIN_2FA_ALLOW_MFA_BYPASS_BACKENDS = [ - "mozilla_django_oidc_db.backends.OIDCAuthenticationBackend", -] - -# -# Mozilla Django OIDC DB settings -# -OIDC_AUTHENTICATE_CLASS = "mozilla_django_oidc_db.views.OIDCAuthenticationRequestView" -MOZILLA_DJANGO_OIDC_DB_CACHE = "oidc" -MOZILLA_DJANGO_OIDC_DB_CACHE_TIMEOUT = 5 * 60 - -if config("DISABLE_2FA", default=False): # pragma: no cover - MAYKIN_2FA_ALLOW_MFA_BYPASS_BACKENDS = AUTHENTICATION_BACKENDS - # # Django setup configuration # diff --git a/src/objecttypes/conf/docker.py b/src/objecttypes/conf/docker.py index efd2b87c..55673759 100644 --- a/src/objecttypes/conf/docker.py +++ b/src/objecttypes/conf/docker.py @@ -1,57 +1,11 @@ import os -os.environ.setdefault("DB_USER", os.getenv("DB_USER", "objecttypes")) -os.environ.setdefault("DB_NAME", os.getenv("DB_NAME", "objecttypes")) -os.environ.setdefault("DB_PASSWORD", os.getenv("DB_PASSWORD", "objecttypes")) -os.environ.setdefault("DB_HOST", os.getenv("DB_HOST", "db")) -os.environ.setdefault("ENVIRONMENT", "docker") - -from .base import * # noqa isort:skip -from .utils import config # noqa isort:skip - -# -# Standard Django settings. -# - -CACHES = { - "default": { - "BACKEND": "django.core.cache.backends.locmem.LocMemCache", - }, - # https://github.com/jazzband/django-axes/blob/master/docs/configuration.rst#cache-problems - "axes_cache": { - "BACKEND": "django.core.cache.backends.dummy.DummyCache", - }, - "oidc": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}, -} - -# Deal with being hosted on a subpath -subpath = config("SUBPATH", None) -if subpath: - if not subpath.startswith("/"): - subpath = f"/{subpath}" - - FORCE_SCRIPT_NAME = subpath - STATIC_URL = f"{FORCE_SCRIPT_NAME}{STATIC_URL}" - MEDIA_URL = f"{FORCE_SCRIPT_NAME}{MEDIA_URL}" +os.environ.setdefault("DB_HOST", "db") +os.environ.setdefault("DB_NAME", "postgres") +os.environ.setdefault("DB_USER", "postgres") +os.environ.setdefault("DB_PASSWORD", "") +os.environ.setdefault("ENVIRONMENT", "docker") +os.environ.setdefault("LOG_STDOUT", "yes") -# -# Additional Django settings -# - -# Disable security measures for development -SESSION_COOKIE_SECURE = config("SESSION_COOKIE_SECURE", False) -SESSION_COOKIE_HTTPONLY = config("SESSION_COOKIE_HTTPONLY", False) -CSRF_COOKIE_SECURE = config("CSRF_COOKIE_SECURE", False) - - -# -# Library settings -# - -# django-axes -AXES_BEHIND_REVERSE_PROXY = False -AXES_CACHE = "axes_cache" - -# Elastic APM -ELASTIC_APM["SERVICE_NAME"] += " " + ENVIRONMENT +from .production import * # noqa isort:skip diff --git a/src/objecttypes/conf/production.py b/src/objecttypes/conf/production.py new file mode 100644 index 00000000..9ef1b1d9 --- /dev/null +++ b/src/objecttypes/conf/production.py @@ -0,0 +1,48 @@ +import os + +os.environ.setdefault("ENVIRONMENT", "production") + +from .base import * # noqa isort:skip + +# +# Standard Django settings. +# + +# Caching sessions. +SESSION_ENGINE = "django.contrib.sessions.backends.cache" +SESSION_CACHE_ALIAS = "default" + +# Caching templates. +TEMPLATES[0]["OPTIONS"]["loaders"] = [ + ("django.template.loaders.cached.Loader", TEMPLATE_LOADERS) +] + +# The file storage engine to use when collecting static files with the +# collectstatic management command. +STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage" + +# Production logging facility. +LOGGING["loggers"].update( + { + "django.security.DisallowedHost": { + "handlers": _django_handlers, + "level": "CRITICAL", + "propagate": False, + }, + } +) + +# Only set this when we're behind a reverse proxy +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") +SECURE_CONTENT_TYPE_NOSNIFF = True # Sets X-Content-Type-Options: nosniff +SECURE_BROWSER_XSS_FILTER = True # Sets X-XSS-Protection: 1; mode=block + +# Deal with being hosted on a subpath +if subpath and subpath != "/": + STATIC_URL = f"{subpath}{STATIC_URL}" + MEDIA_URL = f"{subpath}{MEDIA_URL}" + +# +# Custom settings overrides +# +ENVIRONMENT_SHOWN_IN_ADMIN = False diff --git a/src/objecttypes/conf/staging.py b/src/objecttypes/conf/staging.py new file mode 100644 index 00000000..b9f88fb0 --- /dev/null +++ b/src/objecttypes/conf/staging.py @@ -0,0 +1,10 @@ +""" +Staging environment settings module. + +This *should* be nearly identical to production. +""" +import os + +os.environ.setdefault("ENVIRONMENT", "staging") + +from .production import * # noqa diff --git a/src/objecttypes/conf/tests/__init__.py b/src/objecttypes/conf/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/objecttypes/conf/tests/test_config_helper.py b/src/objecttypes/conf/tests/test_config_helper.py deleted file mode 100644 index 198e684c..00000000 --- a/src/objecttypes/conf/tests/test_config_helper.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.test import SimpleTestCase - -from ..utils import config - - -class ConfigHelperTests(SimpleTestCase): - def test_empty_list_as_default(self): - value = config("SOME_TEST_ENVVAR", split=True, default=[]) - - self.assertEqual(value, []) - - def test_non_empty_list_as_default(self): - value = config("SOME_TEST_ENVVAR", split=True, default=["foo"]) - - self.assertEqual(value, ["foo"]) diff --git a/src/objecttypes/conf/utils.py b/src/objecttypes/conf/utils.py deleted file mode 100644 index 86dd7cb7..00000000 --- a/src/objecttypes/conf/utils.py +++ /dev/null @@ -1,26 +0,0 @@ -import logging - -from decouple import Csv, config as _config, undefined - -logger = logging.getLogger(__name__) - - -def config(option: str, default=undefined, *args, **kwargs): - """ - Pull a config parameter from the environment. - - Read the config variable ``option``. If it's optional, use the ``default`` value. - Input is automatically cast to the correct type, where the type is derived from the - default value if possible. - - Pass ``split=True`` to split the comma-separated input into a list. - """ - if "split" in kwargs: - kwargs.pop("split") - kwargs["cast"] = Csv() - if isinstance(default, list): - default = ",".join(default) - - if default is not undefined and default is not None: - kwargs.setdefault("cast", type(default)) - return _config(option, default=default, *args, **kwargs)