Files
lasuite-docs/src/backend/impress/settings.py
T
Manuel RaynaudandAnthony LC 7c4a260a15 (backend) add a service generating cached RS256 JWT tokens
We want to generate jwt token using the RS256 algotrithm. This token
will be used for internal call with the yhub service.
2026-09-01 15:15:34 +02:00

1543 lines
52 KiB
Python
Executable File

"""
Django settings for impress project.
Generated by 'django-admin startproject' using Django 3.1.5.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
import os
import tomllib
from socket import gethostbyname, gethostname
from django.utils.translation import gettext_lazy as _
import dj_database_url
import posthog
import sentry_sdk
from configurations import Configuration, values
from corsheaders.defaults import default_headers
from csp.constants import NONE
from lasuite.configuration.values import SecretFileValue
from lasuite.oidc_login.enums import OIDCUserEndpointFormat
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.logging import ignore_logger
# pylint: disable=too-many-lines
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATA_DIR = os.getenv("DATA_DIR", os.path.join("/", "data"))
KB = 1024
MB = KB * KB
GB = MB * KB
def get_release():
"""
Get the current release of the application
"""
try:
with open(os.path.join(BASE_DIR, "pyproject.toml"), "rb") as f:
pyproject_data = tomllib.load(f)
return pyproject_data["project"]["version"]
except FileNotFoundError, KeyError:
return "NA" # Default: not available
class Base(Configuration):
"""
This is the base configuration every configuration (aka environment) should inherit from. It
is recommended to configure third-party applications by creating a configuration mixins in
./configurations and compose the Base configuration with those mixins.
It depends on an environment variable that SHOULD be defined:
* DJANGO_SECRET_KEY
You may also want to override default configuration by setting the following environment
variables:
* SENTRY_DSN
* DB_NAME
* DB_HOST
* DB_PASSWORD
* DB_USER
"""
DEBUG = False
USE_SWAGGER = False
API_VERSION = "v1.0"
# Security
ALLOWED_HOSTS = values.ListValue([])
SECRET_KEY = SecretFileValue(None)
SERVER_TO_SERVER_API_TOKENS = values.ListValue([])
# Application definition
ROOT_URLCONF = "impress.urls"
WSGI_APPLICATION = "impress.wsgi.application"
# Database
DATABASES = {
"default": dj_database_url.config()
if values.DatabaseURLValue(
None, environ_name="DATABASE_URL", environ_prefix=None
)
else {
"ENGINE": values.Value(
"django.db.backends.postgresql",
environ_name="DB_ENGINE",
environ_prefix=None,
),
"NAME": values.Value(
"impress", environ_name="DB_NAME", environ_prefix=None
),
"USER": values.Value("dinum", environ_name="DB_USER", environ_prefix=None),
"PASSWORD": SecretFileValue(
"pass", environ_name="DB_PASSWORD", environ_prefix=None
),
"HOST": values.Value(
"localhost", environ_name="DB_HOST", environ_prefix=None
),
"PORT": values.Value(5432, environ_name="DB_PORT", environ_prefix=None),
# Psycopg pool can be configured in the post_setup method
}
}
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Search
SEARCH_INDEXER_CLASS = values.Value(
default=None,
environ_name="SEARCH_INDEXER_CLASS",
environ_prefix=None,
)
SEARCH_INDEXER_BATCH_SIZE = values.IntegerValue(
default=100_000, environ_name="SEARCH_INDEXER_BATCH_SIZE", environ_prefix=None
)
INDEXING_URL = values.Value(
default=None, environ_name="INDEXING_URL", environ_prefix=None
)
SEARCH_INDEXER_COUNTDOWN = values.IntegerValue(
default=1, environ_name="SEARCH_INDEXER_COUNTDOWN", environ_prefix=None
)
SEARCH_INDEXER_SECRET = values.Value(
default=None, environ_name="SEARCH_INDEXER_SECRET", environ_prefix=None
)
SEARCH_URL = values.Value(
default=None, environ_name="SEARCH_URL", environ_prefix=None
)
SEARCH_INDEXER_QUERY_LIMIT = values.PositiveIntegerValue(
default=50, environ_name="SEARCH_INDEXER_QUERY_LIMIT", environ_prefix=None
)
MEDIA_AUTH_ORIGINAL_URL_HEADER = values.Value(
default="HTTP_X_ORIGINAL_URL",
environ_name="MEDIA_AUTH_ORIGINAL_URL_HEADER",
environ_prefix=None,
)
# Static files (CSS, JavaScript, Images)
STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(DATA_DIR, "static")
MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(DATA_DIR, "media")
MEDIA_BASE_URL = values.Value(
None, environ_name="MEDIA_BASE_URL", environ_prefix=None
)
SITE_ID = 1
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3.S3Storage",
},
"staticfiles": {
"BACKEND": values.Value(
"whitenoise.storage.CompressedManifestStaticFilesStorage",
environ_name="STORAGES_STATICFILES_BACKEND",
),
},
# django-silk looks up its binary cProfile (.prof) storage under this
# exact alias (see silk.models). Routing it through the S3 backend keeps
# profiling artifacts off the pod filesystem, which is read-only /
# ephemeral in Kubernetes; the `silk/` prefix isolates them in the
# bucket. Only used when SILK_ENABLED and the binary profiler are on.
"SILKY_STORAGE": {
"BACKEND": "storages.backends.s3.S3Storage",
"OPTIONS": {"location": "silk"},
},
}
# Media
AWS_S3_ENDPOINT_URL = values.Value(
environ_name="AWS_S3_ENDPOINT_URL", environ_prefix=None
)
AWS_S3_ACCESS_KEY_ID = SecretFileValue(
environ_name="AWS_S3_ACCESS_KEY_ID", environ_prefix=None
)
AWS_S3_SECRET_ACCESS_KEY = SecretFileValue(
environ_name="AWS_S3_SECRET_ACCESS_KEY", environ_prefix=None
)
AWS_S3_REGION_NAME = values.Value(
environ_name="AWS_S3_REGION_NAME", environ_prefix=None
)
AWS_STORAGE_BUCKET_NAME = values.Value(
"impress-media-storage",
environ_name="AWS_STORAGE_BUCKET_NAME",
environ_prefix=None,
)
AWS_S3_SIGNATURE_VERSION = values.Value(
"s3v4",
environ_name="AWS_S3_SIGNATURE_VERSION",
environ_prefix=None,
)
# Document images
DOCUMENT_IMAGE_MAX_SIZE = values.IntegerValue(
10 * MB, # 10MB
environ_name="DOCUMENT_IMAGE_MAX_SIZE",
environ_prefix=None,
)
REACTIONS_MAX_PER_COMMENT = values.IntegerValue(
15,
environ_name="REACTIONS_MAX_PER_COMMENT",
environ_prefix=None,
)
DOCUMENT_UNSAFE_MIME_TYPES = [
# Executable Files
"application/x-msdownload",
"application/x-bat",
"application/x-dosexec",
"application/x-sh",
"application/x-ms-dos-executable",
"application/x-msi",
"application/java-archive",
"application/octet-stream",
# Dynamic Web Pages
"application/x-httpd-php",
"application/x-asp",
"application/x-aspx",
"application/jsp",
"application/xhtml+xml",
"application/x-python-code",
"application/x-perl",
"text/html",
"text/javascript",
"text/x-php",
# System Files
"application/x-msdownload",
"application/x-sys",
"application/x-drv",
"application/cpl",
"application/x-apple-diskimage",
# Script Files
"application/javascript",
"application/x-vbscript",
"application/x-powershell",
"application/x-shellscript",
# Compressed/Archive Files
"application/zip",
"application/x-tar",
"application/gzip",
"application/x-bzip2",
"application/x-7z-compressed",
"application/x-rar",
"application/x-rar-compressed",
"application/x-compress",
"application/x-lzma",
# Macros in Documents
"application/vnd.ms-word",
"application/vnd.ms-excel",
"application/vnd.ms-powerpoint",
"application/vnd.ms-word.document.macroenabled.12",
"application/vnd.ms-excel.sheet.macroenabled.12",
"application/vnd.ms-powerpoint.presentation.macroenabled.12",
# Disk Images & Virtual Disk Files
"application/x-iso9660-image",
"application/x-vmdk",
"application/x-apple-diskimage",
"application/x-dmg",
# Other Dangerous MIME Types
"application/x-ms-application",
"application/x-msdownload",
"application/x-shockwave-flash",
"application/x-silverlight-app",
"application/x-java-vm",
"application/x-bittorrent",
"application/hta",
"application/x-csh",
"application/x-ksh",
"application/x-ms-regedit",
"application/x-msdownload",
"application/xml",
]
DOCUMENT_ATTACHMENT_CHECK_UNSAFE_MIME_TYPES_ENABLED = values.BooleanValue(
True,
environ_name="DOCUMENT_ATTACHMENT_CHECK_UNSAFE_MIME_TYPES_ENABLED",
environ_prefix=None,
)
# Document versions
DOCUMENT_VERSIONS_PAGE_SIZE = 50
# Document /all endpoint
DOCUMENT_ALL_ENDPOINT_ENABLED = values.BooleanValue(
default=True,
environ_name="DOCUMENT_ALL_ENDPOINT_ENABLED",
environ_prefix=None,
)
# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/
# Languages
LANGUAGE_CODE = values.Value("en-us")
# cookie & language is set from frontend
LANGUAGE_COOKIE_NAME = "docs_language"
LANGUAGE_COOKIE_PATH = "/"
DRF_NESTED_MULTIPART_PARSER = {
# output of parser is converted to querydict
# if is set to False, dict python is returned
"querydict": False,
}
# Careful! Languages should be ordered by priority, as this tuple is used to get
# fallback/default languages throughout the app.
LANGUAGES = values.SingleNestedTupleValue(
(
("en-us", "English"),
("fr-fr", "Français"),
("de-de", "Deutsch"),
("nl-nl", "Nederlands"),
("es-es", "Español"),
)
)
LOCALE_PATHS = (os.path.join(BASE_DIR, "locale"),)
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Templates
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [os.path.join(BASE_DIR, "templates")],
"OPTIONS": {
"context_processors": [
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"django.template.context_processors.csrf",
"django.template.context_processors.debug",
"django.template.context_processors.i18n",
"django.template.context_processors.media",
"django.template.context_processors.request",
"django.template.context_processors.tz",
],
"loaders": [
"django.template.loaders.filesystem.Loader",
"django.template.loaders.app_directories.Loader",
],
},
},
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"core.middleware.ForceSessionMiddleware",
"core.middleware.SaveRawBodyMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"dockerflow.django.middleware.DockerflowMiddleware",
"csp.middleware.CSPMiddleware",
"waffle.middleware.WaffleMiddleware",
]
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"core.authentication.backends.OIDCAuthenticationBackend",
]
# Django applications from the highest priority to the lowest
INSTALLED_APPS = [
# impress
"core",
"demo",
"drf_spectacular",
# Third party apps
"corsheaders",
"django_filters",
"dockerflow.django",
"rest_framework",
"parler",
"treebeard",
"easy_thumbnails",
"waffle",
# Django
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.postgres",
"django.contrib.sessions",
"django.contrib.sites",
"django.contrib.messages",
"django.contrib.staticfiles",
# OIDC third party
"mozilla_django_oidc",
"lasuite.malware_detection",
"lasuite.marketing",
"csp",
]
# Cache
CACHES = {
"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"},
}
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"core.authentication.backends.SessionAuthentication",
),
"DEFAULT_PARSER_CLASSES": [
"rest_framework.parsers.JSONParser",
"nested_multipart_parser.drf.DrfNestedParser",
],
"DEFAULT_RENDERER_CLASSES": [
# 🔒️ Disable BrowsableAPIRenderer which provides forms allowing a user to
# see all the data in the database (ie a serializer with a ForeignKey field
# will generate a form with a field with all possible values of the FK).
"rest_framework.renderers.JSONRenderer",
],
"EXCEPTION_HANDLER": "core.api.exception_handler",
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning",
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_THROTTLE_CLASSES": [
"lasuite.drf.throttling.MonitoredScopedRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"user_list_sustained": values.Value(
default="180/hour",
environ_name="API_USERS_LIST_THROTTLE_RATE_SUSTAINED",
environ_prefix=None,
),
"user_list_burst": values.Value(
default="30/minute",
environ_name="API_USERS_LIST_THROTTLE_RATE_BURST",
environ_prefix=None,
),
"document": values.Value(
default="80/minute",
environ_name="API_DOCUMENT_THROTTLE_RATE",
environ_prefix=None,
),
"document_access": values.Value(
default="50/minute",
environ_name="API_DOCUMENT_ACCESS_THROTTLE_RATE",
environ_prefix=None,
),
"invitation": values.Value(
default="60/minute",
environ_name="API_INVITATION_THROTTLE_RATE",
environ_prefix=None,
),
"document_ask_for_access": values.Value(
default="30/minute",
environ_name="API_DOCUMENT_ASK_FOR_ACCESS_THROTTLE_RATE",
environ_prefix=None,
),
"config": values.Value(
default="30/minute",
environ_name="API_CONFIG_THROTTLE_RATE",
environ_prefix=None,
),
},
}
MONITORED_THROTTLE_FAILURE_CALLBACK = (
"core.api.throttling.sentry_monitoring_throttle_failure"
)
SPECTACULAR_SETTINGS = {
"TITLE": "Impress API",
"DESCRIPTION": "This is the impress API schema.",
"VERSION": "1.0.0",
"SERVE_INCLUDE_SCHEMA": False,
"ENABLE_DJANGO_DEPLOY_CHECK": values.BooleanValue(
default=False,
environ_name="SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK",
),
"COMPONENT_SPLIT_REQUEST": True,
# OTHER SETTINGS
"SWAGGER_UI_DIST": "SIDECAR", # shorthand to use the sidecar instead
"SWAGGER_UI_FAVICON_HREF": "SIDECAR",
"REDOC_DIST": "SIDECAR",
}
TRASHBIN_CUTOFF_DAYS = values.IntegerValue(
30, environ_name="TRASHBIN_CUTOFF_DAYS", environ_prefix=None
)
# Mail
EMAIL_BACKEND = values.Value("django.core.mail.backends.smtp.EmailBackend")
EMAIL_BRAND_NAME = values.Value(None)
EMAIL_HOST = values.Value(None)
EMAIL_HOST_USER = values.Value(None)
EMAIL_HOST_PASSWORD = SecretFileValue(None)
EMAIL_LOGO_IMG = values.Value(None)
EMAIL_PORT = values.PositiveIntegerValue(None)
EMAIL_URL_APP = values.Value(None)
EMAIL_USE_TLS = values.BooleanValue(False)
EMAIL_USE_SSL = values.BooleanValue(False)
EMAIL_FROM = values.Value("from@example.com")
AUTH_USER_MODEL = "core.User"
INVITATION_VALIDITY_DURATION = 604800 # 7 days, in seconds
# CORS
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_ALL_ORIGINS = values.BooleanValue(False)
CORS_ALLOWED_ORIGINS = values.ListValue([])
CORS_ALLOWED_ORIGIN_REGEXES = values.ListValue([])
# Sentry
SENTRY_DSN = values.Value(None, environ_name="SENTRY_DSN", environ_prefix=None)
# Collaboration
# TODO(yhub): unused since the yhub migration — yhub has no management API
# (reset-connections / get-connections). Kept until a yhub kick and
# connection-info API exist and CollaborationService is reinstated.
COLLABORATION_API_URL = values.Value(
None, environ_name="COLLABORATION_API_URL", environ_prefix=None
)
# TODO(yhub): unused since the yhub migration, see COLLABORATION_API_URL.
COLLABORATION_SERVER_SECRET = SecretFileValue(
None, environ_name="COLLABORATION_SERVER_SECRET", environ_prefix=None
)
COLLABORATION_WS_URL = values.Value(
None, environ_name="COLLABORATION_WS_URL", environ_prefix=None
)
COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = values.BooleanValue(
default=values.BooleanValue( # COLLABORATION_WS_NOT_CONNECTED_READY_ONLY compat
default=False,
environ_name="COLLABORATION_WS_NOT_CONNECTED_READY_ONLY",
environ_prefix=None,
),
environ_name="COLLABORATION_WS_NOT_CONNECTED_READ_ONLY",
environ_prefix=None,
)
COLLABORATION_WS_INACTIVITY_TIMEOUT = values.IntegerValue(
None,
environ_name="COLLABORATION_WS_INACTIVITY_TIMEOUT",
environ_prefix=None,
)
# JWT
# RSA private key (PEM) used to sign the tokens issued by
# core.services.jwt_services.JWTService. Prefer the JWT_PRIVATE_KEY_FILE
# environment variable, a PEM does not fit well in an environment variable.
JWT_PRIVATE_KEY = SecretFileValue(
None,
environ_name="JWT_PRIVATE_KEY",
environ_prefix=None,
)
# Lifetime, in seconds, of the tokens issued by the JWT service. It is both
# the "exp" claim horizon and the cache timeout of the generated tokens.
JWT_TOKEN_LIFETIME = values.IntegerValue(
default=3600,
environ_name="JWT_TOKEN_LIFETIME",
environ_prefix=None,
)
# Frontend
FRONTEND_THEME = values.Value(
None, environ_name="FRONTEND_THEME", environ_prefix=None
)
FRONTEND_HOMEPAGE_FEATURE_ENABLED = values.BooleanValue(
default=True,
environ_name="FRONTEND_HOMEPAGE_FEATURE_ENABLED",
environ_prefix=None,
)
FRONTEND_CSS_URL = values.Value(
None, environ_name="FRONTEND_CSS_URL", environ_prefix=None
)
FRONTEND_JS_URL = values.Value(
None, environ_name="FRONTEND_JS_URL", environ_prefix=None
)
FRONTEND_SILENT_LOGIN_ENABLED = values.BooleanValue(
default=False, environ_name="FRONTEND_SILENT_LOGIN_ENABLED", environ_prefix=None
)
THEME_CUSTOMIZATION_FILE_PATH = values.Value(
os.path.join(BASE_DIR, "impress/configuration/theme/default.json"),
environ_name="THEME_CUSTOMIZATION_FILE_PATH",
environ_prefix=None,
)
THEME_CUSTOMIZATION_CACHE_TIMEOUT = values.IntegerValue(
60 * 60 * 24,
environ_name="THEME_CUSTOMIZATION_CACHE_TIMEOUT",
environ_prefix=None,
)
# Posthog
POSTHOG_KEY = SecretFileValue(None, environ_name="POSTHOG_KEY", environ_prefix=None)
POSTHOG_HOST = values.Value(
"https://eu.i.posthog.com", environ_name="POSTHOG_HOST", environ_prefix=None
)
# Easy thumbnails
THUMBNAIL_EXTENSION = "webp"
THUMBNAIL_TRANSPARENCY_EXTENSION = "webp"
THUMBNAIL_DEFAULT_STORAGE_ALIAS = "default"
THUMBNAIL_ALIASES = {}
# Celery
CELERY_BROKER_URL = values.Value("redis://redis:6379/0")
CELERY_BROKER_TRANSPORT_OPTIONS = values.DictValue({})
# Session
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"
SESSION_COOKIE_AGE = values.PositiveIntegerValue(
default=60 * 60 * 12, environ_name="SESSION_COOKIE_AGE", environ_prefix=None
)
SESSION_COOKIE_NAME = "docs_sessionid"
# OIDC - Authorization Code Flow
OIDC_AUTHENTICATE_CLASS = values.Value(
"lasuite.oidc_login.views.OIDCAuthenticationRequestView",
environ_name="OIDC_AUTHENTICATE_CLASS",
environ_prefix=None,
)
OIDC_CALLBACK_CLASS = values.Value(
"lasuite.oidc_login.views.OIDCAuthenticationCallbackView",
environ_name="OIDC_CALLBACK_CLASS",
environ_prefix=None,
)
OIDC_CREATE_USER = values.BooleanValue(
default=True,
environ_name="OIDC_CREATE_USER",
)
OIDC_RP_SIGN_ALGO = values.Value(
"RS256", environ_name="OIDC_RP_SIGN_ALGO", environ_prefix=None
)
OIDC_RP_CLIENT_ID = values.Value(
"impress", environ_name="OIDC_RP_CLIENT_ID", environ_prefix=None
)
OIDC_RP_CLIENT_SECRET = SecretFileValue(
None,
environ_name="OIDC_RP_CLIENT_SECRET",
environ_prefix=None,
)
OIDC_OP_JWKS_ENDPOINT = values.Value(
environ_name="OIDC_OP_JWKS_ENDPOINT", environ_prefix=None
)
OIDC_OP_AUTHORIZATION_ENDPOINT = values.Value(
environ_name="OIDC_OP_AUTHORIZATION_ENDPOINT", environ_prefix=None
)
OIDC_OP_TOKEN_ENDPOINT = values.Value(
None, environ_name="OIDC_OP_TOKEN_ENDPOINT", environ_prefix=None
)
OIDC_OP_USER_ENDPOINT = values.Value(
None, environ_name="OIDC_OP_USER_ENDPOINT", environ_prefix=None
)
OIDC_OP_USER_ENDPOINT_FORMAT = values.Value(
OIDCUserEndpointFormat.AUTO.name,
environ_name="OIDC_OP_USER_ENDPOINT_FORMAT",
eviron_prefix=None,
choices=[e.name for e in OIDCUserEndpointFormat],
)
OIDC_OP_LOGOUT_ENDPOINT = values.Value(
None, environ_name="OIDC_OP_LOGOUT_ENDPOINT", environ_prefix=None
)
OIDC_AUTH_REQUEST_EXTRA_PARAMS = values.DictValue(
{}, environ_name="OIDC_AUTH_REQUEST_EXTRA_PARAMS", environ_prefix=None
)
OIDC_RP_SCOPES = values.Value(
"openid email", environ_name="OIDC_RP_SCOPES", environ_prefix=None
)
LOGIN_REDIRECT_URL = values.Value(
None, environ_name="LOGIN_REDIRECT_URL", environ_prefix=None
)
LOGIN_REDIRECT_URL_FAILURE = values.Value(
None, environ_name="LOGIN_REDIRECT_URL_FAILURE", environ_prefix=None
)
LOGOUT_REDIRECT_URL = values.Value(
None, environ_name="LOGOUT_REDIRECT_URL", environ_prefix=None
)
OIDC_USE_NONCE = values.BooleanValue(
default=True, environ_name="OIDC_USE_NONCE", environ_prefix=None
)
OIDC_REDIRECT_REQUIRE_HTTPS = values.BooleanValue(
default=False, environ_name="OIDC_REDIRECT_REQUIRE_HTTPS", environ_prefix=None
)
OIDC_REDIRECT_ALLOWED_HOSTS = values.ListValue(
default=[], environ_name="OIDC_REDIRECT_ALLOWED_HOSTS", environ_prefix=None
)
OIDC_STORE_ID_TOKEN = values.BooleanValue(
default=True, environ_name="OIDC_STORE_ID_TOKEN", environ_prefix=None
)
OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = values.BooleanValue(
default=True,
environ_name="OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION",
environ_prefix=None,
)
OIDC_USE_PKCE = values.BooleanValue(
default=False, environ_name="OIDC_USE_PKCE", environ_prefix=None
)
OIDC_PKCE_CODE_CHALLENGE_METHOD = values.Value(
default="S256",
environ_name="OIDC_PKCE_CODE_CHALLENGE_METHOD",
environ_prefix=None,
)
OIDC_PKCE_CODE_VERIFIER_SIZE = values.IntegerValue(
default=64, environ_name="OIDC_PKCE_CODE_VERIFIER_SIZE", environ_prefix=None
)
OIDC_STORE_ACCESS_TOKEN = values.BooleanValue(
default=False, environ_name="OIDC_STORE_ACCESS_TOKEN", environ_prefix=None
)
OIDC_STORE_REFRESH_TOKEN = values.BooleanValue(
default=False, environ_name="OIDC_STORE_REFRESH_TOKEN", environ_prefix=None
)
OIDC_STORE_REFRESH_TOKEN_KEY = values.Value(
default=None,
environ_name="OIDC_STORE_REFRESH_TOKEN_KEY",
environ_prefix=None,
)
# WARNING: Enabling this setting allows multiple user accounts to share the same email
# address. This may cause security issues and is not recommended for production use when
# email is activated as fallback for identification (see previous setting).
OIDC_ALLOW_DUPLICATE_EMAILS = values.BooleanValue(
default=False,
environ_name="OIDC_ALLOW_DUPLICATE_EMAILS",
environ_prefix=None,
)
USER_OIDC_ESSENTIAL_CLAIMS = values.ListValue(
default=[], environ_name="USER_OIDC_ESSENTIAL_CLAIMS", environ_prefix=None
)
OIDC_USERINFO_FULLNAME_FIELDS = values.ListValue(
default=values.ListValue( # retrocompatibility
default=["first_name", "last_name"],
environ_name="USER_OIDC_FIELDS_TO_FULLNAME",
environ_prefix=None,
),
environ_name="OIDC_USERINFO_FULLNAME_FIELDS",
environ_prefix=None,
)
OIDC_USERINFO_SHORTNAME_FIELD = values.Value(
default=values.Value( # retrocompatibility
default="first_name",
environ_name="USER_OIDC_FIELD_TO_SHORTNAME",
environ_prefix=None,
),
environ_name="OIDC_USERINFO_SHORTNAME_FIELD",
environ_prefix=None,
)
# OIDC Resource Server
OIDC_RESOURCE_SERVER_ENABLED = values.BooleanValue(
default=False, environ_name="OIDC_RESOURCE_SERVER_ENABLED", environ_prefix=None
)
OIDC_RS_BACKEND_CLASS = values.Value(
"lasuite.oidc_resource_server.backend.ResourceServerBackend",
environ_name="OIDC_RS_BACKEND_CLASS",
environ_prefix=None,
)
OIDC_OP_URL = values.Value(None, environ_name="OIDC_OP_URL", environ_prefix=None)
OIDC_VERIFY_SSL = values.BooleanValue(
default=True, environ_name="OIDC_VERIFY_SSL", environ_prefix=None
)
OIDC_TIMEOUT = values.PositiveIntegerValue(
3, environ_name="OIDC_TIMEOUT", environ_prefix=None
)
OIDC_PROXY = values.Value(None, environ_name="OIDC_PROXY", environ_prefix=None)
OIDC_OP_INTROSPECTION_ENDPOINT = values.Value(
None, environ_name="OIDC_OP_INTROSPECTION_ENDPOINT", environ_prefix=None
)
OIDC_RS_CLIENT_ID = values.Value(
None, environ_name="OIDC_RS_CLIENT_ID", environ_prefix=None
)
OIDC_RS_CLIENT_SECRET = values.Value(
None, environ_name="OIDC_RS_CLIENT_SECRET", environ_prefix=None
)
OIDC_RS_AUDIENCE_CLAIM = values.Value(
"client_id", environ_name="OIDC_RS_AUDIENCE_CLAIM", environ_prefix=None
)
OIDC_RS_ENCRYPTION_ENCODING = values.Value(
"A256GCM", environ_name="OIDC_RS_ENCRYPTION_ENCODING", environ_prefix=None
)
OIDC_RS_ENCRYPTION_ALGO = values.Value(
"RSA-OAEP", environ_name="OIDC_RS_ENCRYPTION_ALGO", environ_prefix=None
)
OIDC_RS_SIGNING_ALGO = values.Value(
"ES256", environ_name="OIDC_RS_SIGNING_ALGO", environ_prefix=None
)
OIDC_RS_SCOPES = values.ListValue(
["openid"], environ_name="OIDC_RS_SCOPES", environ_prefix=None
)
OIDC_RS_ALLOWED_AUDIENCES = values.ListValue(
default=[],
environ_name="OIDC_RS_ALLOWED_AUDIENCES",
environ_prefix=None,
)
OIDC_RS_PRIVATE_KEY_STR = values.Value(
default=None,
environ_name="OIDC_RS_PRIVATE_KEY_STR",
environ_prefix=None,
)
OIDC_RS_ENCRYPTION_KEY_TYPE = values.Value(
default="RSA",
environ_name="OIDC_RS_ENCRYPTION_KEY_TYPE",
environ_prefix=None,
)
# External API Configuration
# Configure available routes and actions for external_api endpoints
EXTERNAL_API = values.DictValue(
default={
"documents": {
"enabled": True,
"actions": [
"list",
"retrieve",
"create",
"children",
],
},
"document_access": {
"enabled": False,
"actions": [],
},
"document_invitation": {
"enabled": False,
"actions": [],
},
"users": {
"enabled": True,
"actions": ["get_me"],
},
},
environ_name="EXTERNAL_API",
environ_prefix=None,
)
ALLOW_LOGOUT_GET_METHOD = values.BooleanValue(
default=True, environ_name="ALLOW_LOGOUT_GET_METHOD", environ_prefix=None
)
# AI settings
AI_ALLOW_REACH_FROM = values.Value(
choices=("public", "authenticated", "restricted"),
default="authenticated",
environ_name="AI_ALLOW_REACH_FROM",
environ_prefix=None,
)
MISTRAL_SDK_BASE_URL = values.Value(
None, environ_name="MISTRAL_SDK_BASE_URL", environ_prefix=None
)
MISTRAL_SDK_API_KEY = SecretFileValue(
None, environ_name="MISTRAL_SDK_API_KEY", environ_prefix=None
)
OPENAI_SDK_API_KEY = SecretFileValue(
default=SecretFileValue( # retrocompatibility
None,
environ_name="AI_API_KEY",
environ_prefix=None,
),
environ_name="OPENAI_SDK_API_KEY",
environ_prefix=None,
)
OPENAI_SDK_BASE_URL = values.Value(
default=values.Value( # retrocompatibility
None, environ_name="AI_BASE_URL", environ_prefix=None
),
environ_name="OPENAI_SDK_BASE_URL",
environ_prefix=None,
)
AI_BOT = values.DictValue(
default={
"name": _("Docs AI"),
"color": "#8bc6ff",
},
environ_name="AI_BOT",
environ_prefix=None,
)
AI_DOCUMENT_RATE_THROTTLE_RATES = {
"minute": 5,
"hour": 100,
"day": 500,
}
# Master settings to enable AI features, if you set it to False,
# all AI features will be disabled even if the other settings are enabled.
AI_FEATURE_ENABLED = values.BooleanValue(
default=False, environ_name="AI_FEATURE_ENABLED", environ_prefix=None
)
# Far better UI but more flaky for the moment
# ⚠️ AGPL license, be sure to comply with the Blocknote license
# if you enable it (https://www.blocknotejs.org/)
AI_FEATURE_BLOCKNOTE_ENABLED = values.BooleanValue(
default=False, environ_name="AI_FEATURE_BLOCKNOTE_ENABLED", environ_prefix=None
)
# UI with less features but more stable
# MIT friendly license, you can enable it without worrying about the license
AI_FEATURE_LEGACY_ENABLED = values.BooleanValue(
default=True, environ_name="AI_FEATURE_LEGACY_ENABLED", environ_prefix=None
)
AI_MODEL = values.Value(None, environ_name="AI_MODEL", environ_prefix=None)
AI_VERCEL_SDK_VERSION = values.IntegerValue(
6, environ_name="AI_VERCEL_SDK_VERSION", environ_prefix=None
)
AI_USER_RATE_THROTTLE_RATES = {
"minute": 3,
"hour": 50,
"day": 200,
}
LANGFUSE_SECRET_KEY = SecretFileValue(
None, environ_name="LANGFUSE_SECRET_KEY", environ_prefix=None
)
LANGFUSE_PUBLIC_KEY = values.Value(
None, environ_name="LANGFUSE_PUBLIC_KEY", environ_prefix=None
)
LANGFUSE_BASE_URL = values.Value(
None, environ_name="LANGFUSE_BASE_URL", environ_prefix=None
)
# Y provider microservice
Y_PROVIDER_API_KEY = SecretFileValue(
environ_name="Y_PROVIDER_API_KEY",
environ_prefix=None,
)
Y_PROVIDER_API_BASE_URL = values.Value(
environ_name="Y_PROVIDER_API_BASE_URL",
environ_prefix=None,
)
# DocSpec API microservice
DOCSPEC_API_URL = values.Value(environ_name="DOCSPEC_API_URL", environ_prefix=None)
# Imported file settings
CONVERSION_UPLOAD_ENABLED = values.BooleanValue(
False, environ_name="CONVERSION_UPLOAD_ENABLED", environ_prefix=None
)
CONVERSION_FILE_MAX_SIZE = values.IntegerValue(
20 * MB,
environ_name="CONVERSION_FILE_MAX_SIZE",
environ_prefix=None,
)
CONVERSION_FILE_EXTENSIONS_ALLOWED = values.ListValue(
default=[".docx", ".md"],
environ_name="CONVERSION_FILE_EXTENSIONS_ALLOWED",
environ_prefix=None,
)
# Conversion endpoint
CONVERSION_API_ENDPOINT = values.Value(
default="convert",
environ_name="CONVERSION_API_ENDPOINT",
environ_prefix=None,
)
CONVERSION_API_CONTENT_FIELD = values.Value(
default="content",
environ_name="CONVERSION_API_CONTENT_FIELD",
environ_prefix=None,
)
CONVERSION_API_TIMEOUT = values.Value(
default=30,
environ_name="CONVERSION_API_TIMEOUT",
environ_prefix=None,
)
CONVERSION_API_SECURE = values.Value(
default=False,
environ_name="CONVERSION_API_SECURE",
environ_prefix=None,
)
NO_WEBSOCKET_CACHE_TIMEOUT = values.Value(
default=120,
environ_name="NO_WEBSOCKET_CACHE_TIMEOUT",
environ_prefix=None,
)
# Logging
# We want to make it easy to log to console but by default we log production
# to Sentry and don't want to log to console.
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"simple": {
"format": "{asctime} {name} {levelname} {message}",
"style": "{",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "simple",
},
},
# Override root logger to send it to console
"root": {
"handlers": ["console"],
"level": values.Value(
"INFO", environ_name="LOGGING_LEVEL_LOGGERS_ROOT", environ_prefix=None
),
},
"loggers": {
"core": {
"handlers": ["console"],
"level": values.Value(
"INFO",
environ_name="LOGGING_LEVEL_LOGGERS_APP",
environ_prefix=None,
),
"propagate": True,
},
"docs.security": {
"handlers": ["console"],
"level": values.Value(
"INFO",
environ_name="LOGGING_LEVEL_LOGGERS_SECURITY",
environ_prefix=None,
),
"propagate": True,
},
},
}
MALWARE_DETECTION = {
"BACKEND": values.Value(
"lasuite.malware_detection.backends.dummy.DummyBackend",
environ_name="MALWARE_DETECTION_BACKEND",
environ_prefix=None,
),
"PARAMETERS": values.DictValue(
default={
"callback_path": "core.malware_detection.malware_detection_callback",
},
environ_name="MALWARE_DETECTION_PARAMETERS",
environ_prefix=None,
),
}
API_USERS_LIST_LIMIT = values.PositiveIntegerValue(
default=5,
environ_name="API_USERS_LIST_LIMIT",
environ_prefix=None,
)
API_USERS_SEARCH_QUERY_MIN_LENGTH = values.PositiveIntegerValue(
default=3,
environ_name="API_USERS_SEARCH_QUERY_MIN_LENGTH",
environ_prefix=None,
)
# Content Security Policy
# See https://content-security-policy.com/ for more information.
CONTENT_SECURITY_POLICY = {
"EXCLUDE_URL_PREFIXES": values.ListValue(
["/admin"],
environ_name="CONTENT_SECURITY_POLICY_EXCLUDE_URL_PREFIXES",
environ_prefix=None,
),
"DIRECTIVES": values.DictValue(
default={
"default-src": [NONE],
"script-src": [NONE],
"style-src": [NONE],
"img-src": [NONE],
"connect-src": [NONE],
"font-src": [NONE],
"object-src": [NONE],
"media-src": [NONE],
"frame-src": [NONE],
"child-src": [NONE],
"form-action": [NONE],
"frame-ancestors": [NONE],
"base-uri": [NONE],
"worker-src": [NONE],
"manifest-src": [NONE],
"prefetch-src": [NONE],
},
environ_name="CONTENT_SECURITY_POLICY_DIRECTIVES",
environ_prefix=None,
),
}
# User accounts management
USER_RECONCILIATION_FORM_URL = values.Value(
None, environ_name="USER_RECONCILIATION_FORM_URL", environ_prefix=None
)
USER_ONBOARDING_DOCUMENTS = values.ListValue(
[], environ_name="USER_ONBOARDING_DOCUMENTS", environ_prefix=None
)
USER_ONBOARDING_SANDBOX_DOCUMENT = values.Value(
None, environ_name="USER_ONBOARDING_SANDBOX_DOCUMENT", environ_prefix=None
)
# Marketing and communication settings
SIGNUP_NEW_USER_TO_MARKETING_EMAIL = values.BooleanValue(
False,
environ_name="SIGNUP_NEW_USER_TO_MARKETING_EMAIL",
environ_prefix=None,
help_text=(
"When enabled, new users are automatically added to mailing list "
"for product updates, marketing communications, and customized emails. "
),
)
LASUITE_MARKETING = {
"BACKEND": values.Value(
"lasuite.marketing.backends.dummy.DummyBackend",
environ_name="LASUITE_MARKETING_BACKEND",
environ_prefix=None,
),
"PARAMETERS": values.DictValue(
default={},
environ_name="LASUITE_MARKETING_PARAMETERS",
environ_prefix=None,
),
}
CONTENT_METADATA_CACHE_TIMEOUT = values.IntegerValue(
60 * 60 * 24, environ_name="CONTENT_METADATA_CACHE_TIMEOUT", environ_prefix=None
)
TREEBEARD_PATH_COMPUTE_RETRY_MAX_ATTEMPTS = values.IntegerValue(
10,
environ_name="TREEBEARD_PATH_COMPUTE_RETRY_MAX_ATTEMPTS",
environ_prefix=None,
)
# -- Profiling (django-silk) ---------------------------------------------
# Opt-in request/SQL/cProfile profiler, OFF by default. Turn it on in a
# given environment with SILK_ENABLED=1 (typically a throwaway staging pod
# loaded via `generate_volumetry`, or local dev) to record, per request,
# the SQL it ran with timing + originating stack, and an optional cProfile
# you can download as a binary `.prof`. When enabled, `silk` is appended to
# INSTALLED_APPS, `SilkyMiddleware` is wired near the top of MIDDLEWARE, and
# the UI is served at /silk/ (see impress/urls.py and post_setup below).
#
# NEVER enable against production with real users: silk persists request
# metadata to the database. Request/response BODIES are deliberately never
# stored (the two MAX_*_BODY_SIZE = 0 below) so document content, titles and
# emails cannot leak into the silk tables — only method, path, headers-free
# metadata, SQL and timings are kept.
SILK_ENABLED = values.BooleanValue(
False, environ_name="SILK_ENABLED", environ_prefix=None
)
# Per-request cProfile. Binary output lets you download a `.prof` and open
# it in snakeviz / pstats / tuna offline for a full call graph. The binary
# is written through the SILKY_STORAGE backend (S3, see STORAGES above), not
# the local filesystem, so it works on read-only/ephemeral pods.
SILKY_PYTHON_PROFILER = values.BooleanValue(
True, environ_name="SILK_PYTHON_PROFILER", environ_prefix=None
)
SILKY_PYTHON_PROFILER_BINARY = values.BooleanValue(
True, environ_name="SILK_PYTHON_PROFILER_BINARY", environ_prefix=None
)
# Under load, record only a sample of requests to bound silk's own overhead
# and storage (100 = every request; drop it for a thundering-herd repro).
SILKY_INTERCEPT_PERCENT = values.IntegerValue(
100, environ_name="SILK_INTERCEPT_PERCENT", environ_prefix=None
)
# Ring-buffer the stored requests so a long load run cannot fill the disk.
SILKY_MAX_RECORDED_REQUESTS = values.IntegerValue(
10000, environ_name="SILK_MAX_RECORDED_REQUESTS", environ_prefix=None
)
SILKY_MAX_RECORDED_REQUESTS_CHECK_PERCENT = 10
# Record silk's own per-request overhead so you can subtract it.
SILKY_META = True
# Lock the /silk/ UI behind an authenticated staff session.
SILKY_AUTHENTICATION = True
SILKY_AUTHORISATION = True
# RGPD: never persist request/response bodies (0 bytes kept).
SILKY_MAX_REQUEST_BODY_SIZE = 0
SILKY_MAX_RESPONSE_BODY_SIZE = 0
# pylint: disable=invalid-name
@property
def ENVIRONMENT(self):
"""Environment in which the application is launched."""
return self.__class__.__name__.lower()
# pylint: disable=invalid-name
@property
def RELEASE(self):
"""
Return the release information.
Delegate to the module function to enable easier testing.
"""
return get_release()
# pylint: disable=invalid-name
@property
def PARLER_LANGUAGES(self):
"""
Return languages for Parler computed from the LANGUAGES and LANGUAGE_CODE settings.
"""
return {
self.SITE_ID: tuple({"code": code} for code, _name in self.LANGUAGES),
"default": {
"fallbacks": [self.LANGUAGE_CODE],
"hide_untranslated": False,
},
}
@classmethod
def post_setup(cls):
"""Post setup configuration.
This is the place where you can configure settings that require other
settings to be loaded.
"""
super().post_setup()
# The SENTRY_DSN setting should be available to activate sentry for an environment
if cls.SENTRY_DSN is not None:
sentry_sdk.init(
dsn=cls.SENTRY_DSN,
environment=cls.__name__.lower(),
release=get_release(),
integrations=[DjangoIntegration()],
)
sentry_sdk.set_tag("application", "backend")
# Ignore the logs added by the DockerflowMiddleware
ignore_logger("request.summary")
if (
cls.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION
and cls.OIDC_ALLOW_DUPLICATE_EMAILS
):
raise ValueError(
"Both OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION and "
"OIDC_ALLOW_DUPLICATE_EMAILS cannot be set to True simultaneously. "
)
psycopg_pool_enabled = values.BooleanValue(
False, environ_name="DB_PSYCOPG_POOL_ENABLED", environ_prefix=""
)
if psycopg_pool_enabled:
cls.DATABASES["default"].update(
{
"OPTIONS": {
# https://www.psycopg.org/psycopg3/docs/api/pool.html#psycopg_pool.ConnectionPool
"pool": {
"min_size": values.IntegerValue(
4,
environ_name="DB_PSYCOPG_POOL_MIN_SIZE",
environ_prefix=None,
),
"max_size": values.IntegerValue(
None,
environ_name="DB_PSYCOPG_POOL_MAX_SIZE",
environ_prefix=None,
),
"timeout": values.IntegerValue(
3,
environ_name="DB_PSYCOPG_POOL_TIMEOUT",
environ_prefix=None,
),
}
},
}
)
if cls.OPENAI_SDK_API_KEY and cls.MISTRAL_SDK_API_KEY:
raise ValueError(
"Both OPENAI_SDK and MISTRAL_SDK parameters can not be set simultaneously."
)
if cls.POSTHOG_KEY is not None:
posthog.api_key = cls.POSTHOG_KEY
posthog.host = cls.POSTHOG_HOST
if cls.SILK_ENABLED:
# Activate django-silk only when explicitly turned on for this
# environment. Appending here (rather than in INSTALLED_APPS) keeps
# silk absent from every environment that does not opt in, including
# production. Guards make re-entry (post_setup can run per subclass)
# idempotent.
if "silk" not in cls.INSTALLED_APPS:
cls.INSTALLED_APPS.append("silk")
# SilkyMiddleware must be high enough to time the whole request but
# after AuthenticationMiddleware so it can attribute request.user;
# process_response runs inner-to-outer, so index 1 (just after
# SecurityMiddleware) satisfies both.
if "silk.middleware.SilkyMiddleware" not in cls.MIDDLEWARE:
cls.MIDDLEWARE.insert(1, "silk.middleware.SilkyMiddleware")
class Build(Base):
"""Settings used when the application is built.
This environment should not be used to run the application. Just to build it with non-blocking
settings.
"""
SECRET_KEY = values.Value("DummyKey")
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": values.Value(
"whitenoise.storage.CompressedManifestStaticFilesStorage",
environ_name="STORAGES_STATICFILES_BACKEND",
),
},
}
class Development(Base):
"""
Development environment settings
We set DEBUG to True and configure the server to respond from all hosts.
"""
ALLOWED_HOSTS = ["*"]
CORS_ALLOW_ALL_ORIGINS = True
CSRF_TRUSTED_ORIGINS = values.ListValue(
["http://localhost:8072", "http://localhost:3000"],
environ_name="DJANGO_CSRF_TRUSTED_ORIGINS",
environ_prefix=None,
)
CORS_ALLOW_HEADERS = (
*default_headers,
"if-none-match",
"if-modified-since",
)
CORS_EXPOSE_HEADERS = ["ETag"]
DEBUG = True
USE_SWAGGER = True
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": values.Value(
"redis://redis:6379/2",
environ_name="REDIS_URL",
environ_prefix=None,
),
"TIMEOUT": values.IntegerValue(
30, # timeout in seconds
environ_name="CACHES_DEFAULT_TIMEOUT",
environ_prefix=None,
),
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
},
}
def __init__(self):
# pylint: disable=invalid-name
self.INSTALLED_APPS += ["django_extensions", "drf_spectacular_sidecar"]
self.CONTENT_SECURITY_POLICY["EXCLUDE_URL_PREFIXES"] += [
f"/api/{self.API_VERSION}/swagger",
f"/api/{self.API_VERSION}/redoc",
"/silk",
]
class Test(Base):
"""Test environment settings"""
PASSWORD_HASHERS = [
"django.contrib.auth.hashers.MD5PasswordHasher",
]
USE_SWAGGER = True
# Static files are not used in the test environment
# Tests are raising warnings because the /data/static directory does not exist
STATIC_ROOT = None
CELERY_TASK_ALWAYS_EAGER = values.BooleanValue(True)
def __init__(self):
# pylint: disable=invalid-name
self.INSTALLED_APPS += ["drf_spectacular_sidecar"]
class ContinuousIntegration(Test):
"""
Continuous Integration environment settings
nota bene: it should inherit from the Test environment.
"""
class Production(Base):
"""
Production environment settings
You must define the ALLOWED_HOSTS environment variable in Production
configuration (and derived configurations):
ALLOWED_HOSTS=["foo.com", "foo.fr"]
"""
# Security
# Add allowed host from environment variables.
# The machine hostname is added by default,
# it makes the application pingable by a load balancer on the same machine by example
ALLOWED_HOSTS = [
*values.ListValue([], environ_name="ALLOWED_HOSTS"),
gethostbyname(gethostname()),
]
CSRF_TRUSTED_ORIGINS = values.ListValue([])
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
# SECURE_PROXY_SSL_HEADER allows to fix the scheme in Django's HttpRequest
# object when your application is behind a reverse proxy.
#
# Keep this SECURE_PROXY_SSL_HEADER configuration only if :
# - your Django app is behind a proxy.
# - your proxy strips the X-Forwarded-Proto header from all incoming requests
# - Your proxy sets the X-Forwarded-Proto header and sends it to Django
#
# In other cases, you should comment the following line to avoid security issues.
# SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_HSTS_SECONDS = 60
SECURE_HSTS_PRELOAD = True
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_SSL_REDIRECT = True
SECURE_REDIRECT_EXEMPT = [
"^__lbheartbeat__",
"^__heartbeat__",
]
# Modern browsers require to have the `secure` attribute on cookies with `Samesite=none`
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
SESSION_CACHE_ALIAS = "session"
# Privacy
SECURE_REFERRER_POLICY = "same-origin"
# Conversion API: Always verify SSL in production
CONVERSION_API_SECURE = True
# Cache
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": values.Value(
"redis://redis:6379/0",
environ_name="REDIS_URL",
environ_prefix=None,
),
"TIMEOUT": values.IntegerValue(
30, # timeout in seconds
environ_name="CACHES_DEFAULT_TIMEOUT",
environ_prefix=None,
),
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
"KEY_PREFIX": values.Value(
"docs",
environ_name="CACHES_DEFAULT_KEY_PREFIX",
environ_prefix=None,
),
},
"session": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": values.Value(
"redis://redis:6379/0",
environ_name="REDIS_URL",
environ_prefix=None,
),
"TIMEOUT": values.IntegerValue(
30, # timeout in seconds
environ_name="CACHES_SESSION_TIMEOUT",
environ_prefix=None,
),
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
},
}
class Feature(Production):
"""
Feature environment settings
nota bene: it should inherit from the Production environment.
"""
class Staging(Production):
"""
Staging environment settings
nota bene: it should inherit from the Production environment.
"""
class PreProduction(Production):
"""
Pre-production environment settings
nota bene: it should inherit from the Production environment.
"""
class Demo(Production):
"""
Demonstration environment settings
nota bene: it should inherit from the Production environment.
"""
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}