""" Django settings for calendar 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 sentry_sdk from configurations import Configuration, values from lasuite.configuration.values import SecretFileValue from sentry_sdk.integrations.django import DjangoIntegration # 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.environ.get("DATA_DIR", os.path.join("/", "data")) 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 LOAD_E2E_URLS = False USE_SWAGGER = False API_VERSION = "v1.0" # CalDAV server URL CALDAV_URL = values.Value( "http://caldav:80", environ_name="CALDAV_URL", environ_prefix=None ) # CalDAV API keys for bidirectional authentication # INBOUND: API key for authenticating requests FROM CalDAV server TO Django CALDAV_INBOUND_API_KEY = values.Value( None, environ_name="CALDAV_INBOUND_API_KEY", environ_prefix=None ) # OUTBOUND: API key for authenticating requests FROM Django TO CalDAV server CALDAV_OUTBOUND_API_KEY = values.Value( None, environ_name="CALDAV_OUTBOUND_API_KEY", environ_prefix=None ) # Base URL for CalDAV scheduling callbacks (must be accessible from CalDAV container) # In Docker environments, use the internal Docker network URL (e.g., http://backend:8000) CALDAV_CALLBACK_BASE_URL = values.Value( None, environ_name="CALDAV_CALLBACK_BASE_URL", environ_prefix=None ) # Email configuration # Default settings - override in environment-specific classes EMAIL_BACKEND = values.Value( "django.core.mail.backends.smtp.EmailBackend", environ_name="EMAIL_BACKEND", environ_prefix=None, ) EMAIL_HOST = values.Value( "localhost", environ_name="EMAIL_HOST", environ_prefix=None ) EMAIL_PORT = values.IntegerValue(25, environ_name="EMAIL_PORT", environ_prefix=None) EMAIL_HOST_USER = values.Value( "", environ_name="EMAIL_HOST_USER", environ_prefix=None ) EMAIL_HOST_PASSWORD = SecretFileValue( "", environ_name="EMAIL_HOST_PASSWORD", environ_prefix=None ) EMAIL_USE_TLS = values.BooleanValue( False, environ_name="EMAIL_USE_TLS", environ_prefix=None ) EMAIL_USE_SSL = values.BooleanValue( False, environ_name="EMAIL_USE_SSL", environ_prefix=None ) DEFAULT_FROM_EMAIL = values.Value( "noreply@example.com", environ_name="DEFAULT_FROM_EMAIL", environ_prefix=None ) # Calendar-specific email settings CALENDAR_INVITATION_FROM_EMAIL = values.Value( None, environ_name="CALENDAR_INVITATION_FROM_EMAIL", environ_prefix=None ) APP_NAME = values.Value("Calendrier", environ_name="APP_NAME", environ_prefix=None) APP_URL = values.Value("", environ_name="APP_URL", environ_prefix=None) CALENDAR_ITIP_ENABLED = values.BooleanValue( False, environ_name="CALENDAR_ITIP_ENABLED", environ_prefix=None ) TRANSLATIONS_JSON_PATH = values.Value( "/data/translations.json", environ_name="TRANSLATIONS_JSON_PATH", environ_prefix=None, ) DEFAULT_CALENDAR_COLOR = values.Value( "#3788d8", environ_name="DEFAULT_CALENDAR_COLOR", environ_prefix=None, ) # Security ALLOWED_HOSTS = values.ListValue([]) SECRET_KEY = SecretFileValue(None) SERVER_TO_SERVER_API_TOKENS = values.ListValue([]) # Application definition ROOT_URLCONF = "calendars.urls" WSGI_APPLICATION = "calendars.wsgi.application" # Database DATABASES = { "default": dj_database_url.config() if os.environ.get("DATABASE_URL") else { "ENGINE": values.Value( "django.db.backends.postgresql", environ_name="DB_ENGINE", environ_prefix=None, ), "NAME": values.Value( "calendars", environ_name="DB_NAME", environ_prefix=None ), "USER": values.Value("pgroot", 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), } } DEFAULT_AUTO_FIELD = "django.db.models.AutoField" # Static files (CSS, JavaScript, Images) STATIC_URL = "/static/" STATIC_ROOT = os.path.join(DATA_DIR, "static") MEDIA_URL = "/media/" MEDIA_URL_PREVIEW = "/media/preview/" 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": "django.core.files.storage.FileSystemStorage", }, "staticfiles": { "BACKEND": values.Value( "whitenoise.storage.CompressedManifestStaticFilesStorage", environ_name="STORAGES_STATICFILES_BACKEND", ), }, } # Maximum size of the request body in memory. # This is used to limit the size of the request body in memory. # This also limits the size of the file that can be uploaded to the server. DATA_UPLOAD_MAX_MEMORY_SIZE = values.PositiveIntegerValue( 2 * (2**30), # 2GB environ_name="DATA_UPLOAD_MAX_MEMORY_SIZE", environ_prefix=None, ) # Internationalization # https://docs.djangoproject.com/en/3.1/topics/i18n/ # Languages LANGUAGE_CODE = values.Value("en-us") LANGUAGE_COOKIE_NAME = "calendar_language" # cookie & language is set from frontend 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", _("French")), ("de-de", _("German")), ("nl-nl", _("Dutch")), ) ) 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", "django.contrib.messages.middleware.MessageMiddleware", ] AUTHENTICATION_BACKENDS = [ "django.contrib.auth.backends.ModelBackend", "core.authentication.backends.OIDCAuthenticationBackend", ] # Django applications from the highest priority to the lowest INSTALLED_APPS = [ "core", "drf_spectacular", "drf_standardized_errors", # Third party apps "corsheaders", "django_celery_beat", "django_filters", "rest_framework", "rest_framework_api_key", "parler", # 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", ] # 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( "calendar", 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", }, }, } REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( "mozilla_django_oidc.contrib.drf.OIDCAuthentication", "rest_framework.authentication.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": ["rest_framework.throttling.ScopedRateThrottle"], "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, ), }, } MAX_PAGE_SIZE = values.PositiveIntegerValue( 200, environ_name="MAX_PAGE_SIZE", environ_prefix=None ) SPECTACULAR_SETTINGS = { "TITLE": "calendar API", "DESCRIPTION": "This is the calendar 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.Value( 30, environ_name="TRASHBIN_CUTOFF_DAYS", environ_prefix=None ) 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([]) # Allow CalDAV methods (PROPFIND, PROPPATCH, REPORT, etc.) CORS_ALLOW_METHODS = [ "DELETE", "GET", "OPTIONS", "PATCH", "POST", "PUT", "PROPFIND", "PROPPATCH", "REPORT", "MKCOL", "MKCALENDAR", ] # Allow CalDAV headers (case-sensitive for CORS preflight) CORS_ALLOW_HEADERS = [ "accept", "accept-encoding", "authorization", "content-type", "dnt", "origin", "user-agent", "x-csrftoken", "x-requested-with", "depth", # CalDAV header (lowercase as sent by browsers) "if-match", "if-none-match", "prefer", ] # Sentry SENTRY_DSN = values.Value(None, environ_name="SENTRY_DSN", environ_prefix=None) # Frontend FRONTEND_THEME = values.Value( None, environ_name="FRONTEND_THEME", environ_prefix=None ) FRONTEND_MORE_LINK = values.Value( None, environ_name="FRONTEND_MORE_LINK", environ_prefix=None, ) FRONTEND_FEEDBACK_BUTTON_SHOW = values.BooleanValue( default=False, environ_name="FRONTEND_FEEDBACK_BUTTON_SHOW", environ_prefix=None ) # For instance, you might want to bind this button to an external library to trigger survey instead of the build in feedback modal. FRONTEND_FEEDBACK_BUTTON_IDLE = values.BooleanValue( default=False, environ_name="FRONTEND_FEEDBACK_BUTTON_IDLE", environ_prefix=None ) FRONTEND_FEEDBACK_ITEMS = values.DictValue( {}, environ_name="FRONTEND_FEEDBACK_ITEMS", environ_prefix=None ) FRONTEND_FEEDBACK_MESSAGES_WIDGET_ENABLED = values.BooleanValue( default=False, environ_name="FRONTEND_FEEDBACK_MESSAGES_WIDGET_ENABLED", environ_prefix=None, ) FRONTEND_FEEDBACK_MESSAGES_WIDGET_API_URL = values.Value( None, environ_name="FRONTEND_FEEDBACK_MESSAGES_WIDGET_API_URL", environ_prefix=None, ) FRONTEND_FEEDBACK_MESSAGES_WIDGET_CHANNEL = values.Value( None, environ_name="FRONTEND_FEEDBACK_MESSAGES_WIDGET_CHANNEL", environ_prefix=None, ) FRONTEND_FEEDBACK_MESSAGES_WIDGET_PATH = values.Value( None, environ_name="FRONTEND_FEEDBACK_MESSAGES_WIDGET_PATH", environ_prefix=None ) FRONTEND_HIDE_GAUFRE = values.BooleanValue( default=False, environ_name="FRONTEND_HIDE_GAUFRE", environ_prefix=None ) THEME_CUSTOMIZATION_FILE_PATH = values.Value( os.path.join(BASE_DIR, "calendars/configuration/theme/default.json"), environ_name="THEME_CUSTOMIZATION_FILE_PATH", environ_prefix=None, ) THEME_CUSTOMIZATION_CACHE_TIMEOUT = values.Value( 60 * 60 * 24, environ_name="THEME_CUSTOMIZATION_CACHE_TIMEOUT", 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({}) CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" # Session SESSION_ENGINE = "django.contrib.sessions.backends.cache" SESSION_CACHE_ALIAS = "session" SESSION_COOKIE_AGE = 60 * 60 * 12 # OIDC - Authorization Code Flow OIDC_CREATE_USER = values.BooleanValue( default=True, environ_name="OIDC_CREATE_USER", ) OIDC_CALLBACK_CLASS = "core.authentication.views.OIDCAuthenticationCallbackView" OIDC_RP_SIGN_ALGO = values.Value( "RS256", environ_name="OIDC_RP_SIGN_ALGO", environ_prefix=None ) OIDC_RP_CLIENT_ID = values.Value( "calendar", 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_LOGOUT_ENDPOINT = values.Value( None, environ_name="OIDC_OP_LOGOUT_ENDPOINT", environ_prefix=None ) OIDC_REDIRECT_FIELD_NAME = values.Value( "returnTo", environ_name="OIDC_REDIRECT_FIELD_NAME", 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_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, ) # OIDC claims to store OIDC_STORE_CLAIMS = values.ListValue( default=[], environ_name="OIDC_STORE_CLAIMS", 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, ) OIDC_USER_INFO = values.ListValue( default=values.ListValue( # retrocompatibility default=[], environ_name="USER_OIDC_ESSENTIAL_CLAIMS", environ_prefix=None, ), environ_name="OIDC_USER_INFO", environ_prefix=None, ) OIDC_USERINFO_FULLNAME_FIELDS = values.ListValue( default=["first_name", "last_name"], environ_name="OIDC_USERINFO_FULLNAME_FIELDS", environ_prefix=None, ) OIDC_USERINFO_SHORTNAME_FIELD = values.Value( default="first_name", 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, ) # External API Configuration # Configure available routes and actions for external_api endpoints EXTERNAL_API = values.DictValue( default={ "users": { "enabled": True, "actions": ["get_me"], }, }, environ_name="EXTERNAL_API", 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, ) ALLOW_LOGOUT_GET_METHOD = values.BooleanValue( default=True, environ_name="ALLOW_LOGOUT_GET_METHOD", 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, }, }, } API_USERS_LIST_LIMIT = values.PositiveIntegerValue( default=5, environ_name="API_USERS_LIST_LIMIT", environ_prefix=None, ) # Storage compute # 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") 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. " ) 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. """ SESSION_CACHE_ALIAS = "default" CACHES = { "default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}, } 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 = [ "http://localhost:8920", "http://localhost:3000", ] DEBUG = True LOAD_E2E_URLS = True SESSION_COOKIE_NAME = "calendar_sessionid" USE_SWAGGER = True # Email settings for development (mailcatcher) EMAIL_HOST = "mailcatcher" EMAIL_PORT = 1025 EMAIL_USE_TLS = False EMAIL_USE_SSL = False DEFAULT_FROM_EMAIL = "calendars@calendars.world" CALENDAR_INVITATION_FROM_EMAIL = "calendars@calendars.world" APP_NAME = "Calendrier (Dev)" APP_URL = "http://localhost:8921" DEBUG_TOOLBAR_CONFIG = { "SHOW_TOOLBAR_CALLBACK": lambda request: True, } def __init__(self): # pylint: disable=invalid-name self.MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # pylint: disable=invalid-name self.INSTALLED_APPS += [ "django_extensions", "drf_spectacular_sidecar", "debug_toolbar", "e2e", ] class Test(Base): """Test environment settings""" SESSION_CACHE_ALIAS = "default" CACHES = { "default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}, } PASSWORD_HASHERS = [ "django.contrib.auth.hashers.MD5PasswordHasher", ] USE_SWAGGER = True CELERY_TASK_ALWAYS_EAGER = values.BooleanValue(True) OIDC_STORE_ACCESS_TOKEN = False OIDC_STORE_REFRESH_TOKEN = False def __init__(self): # pylint: disable=invalid-name self.INSTALLED_APPS += ["drf_spectacular_sidecar", "e2e"] 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__", r"^api/v1\.0/caldav-scheduling-callback/", ] # Modern browsers require to have the `secure` attribute on cookies with `Samesite=none` CSRF_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True # Privacy SECURE_REFERRER_POLICY = "same-origin" 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. """