Create new model to allow access of some API endpoints with API Key authentification. Scopes will allow to define permission access on those endpoints.
1071 lines
35 KiB
Python
Executable File
1071 lines
35 KiB
Python
Executable File
"""
|
|
Django's settings for People 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/
|
|
"""
|
|
|
|
# pylint: disable=too-many-lines
|
|
|
|
import json
|
|
import os
|
|
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
import sentry_sdk
|
|
from configurations import Configuration, values
|
|
from cryptography.hazmat.primitives import serialization
|
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
from sentry_sdk.integrations.django import DjangoIntegration
|
|
from sentry_sdk.integrations.logging import ignore_logger
|
|
|
|
# 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.path.join("/", "data")
|
|
|
|
|
|
def get_release():
|
|
"""
|
|
Get the current release of the application
|
|
|
|
By release, we mean the release from the version.json file à la Mozilla [1]
|
|
(if any). If this file has not been found, it defaults to "NA".
|
|
|
|
[1]
|
|
https://github.com/mozilla-services/Dockerflow/blob/master/docs/version_object.md
|
|
"""
|
|
# Try to get the current release from the version.json file generated by the
|
|
# CI during the Docker image build
|
|
try:
|
|
with open(os.path.join(BASE_DIR, "version.json"), encoding="utf8") as version:
|
|
return json.load(version)["version"]
|
|
except FileNotFoundError:
|
|
return "NA" # Default: not available
|
|
|
|
|
|
def get_commit():
|
|
"""
|
|
Get the current commit of the application
|
|
"""
|
|
try:
|
|
with open(os.path.join(BASE_DIR, "version.json"), encoding="utf8") as version:
|
|
return json.load(version)["commit"]
|
|
except FileNotFoundError:
|
|
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:
|
|
|
|
* DJANGO_SENTRY_DSN
|
|
* DB_NAME
|
|
* DB_HOST
|
|
* DB_PASSWORD
|
|
* DB_USER
|
|
"""
|
|
|
|
DEBUG = False
|
|
USE_SWAGGER = values.BooleanValue(
|
|
default=False,
|
|
environ_name="USE_SWAGGER",
|
|
)
|
|
|
|
API_VERSION = "v1.0"
|
|
|
|
# Security
|
|
ALLOWED_HOSTS = values.ListValue([])
|
|
SECRET_KEY = values.Value(None)
|
|
SILENCED_SYSTEM_CHECKS = values.ListValue([])
|
|
|
|
# Application definition
|
|
ROOT_URLCONF = "people.urls"
|
|
WSGI_APPLICATION = "people.wsgi.application"
|
|
|
|
# Database
|
|
DATABASES = {
|
|
"default": {
|
|
"ENGINE": values.Value(
|
|
"django.db.backends.postgresql_psycopg2",
|
|
environ_name="DB_ENGINE",
|
|
environ_prefix=None,
|
|
),
|
|
"NAME": values.Value("people", environ_name="DB_NAME", environ_prefix=None),
|
|
"USER": values.Value("dinum", environ_name="DB_USER", environ_prefix=None),
|
|
"PASSWORD": values.Value(
|
|
"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_ROOT = os.path.join(DATA_DIR, "media")
|
|
|
|
SITE_ID = 1
|
|
|
|
STORAGES = {
|
|
"default": {
|
|
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
|
},
|
|
"staticfiles": {
|
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
|
},
|
|
}
|
|
|
|
# Internationalization
|
|
# https://docs.djangoproject.com/en/3.1/topics/i18n/
|
|
|
|
# Languages
|
|
LANGUAGE_CODE = values.Value(
|
|
default="en-us",
|
|
environ_name="LANGUAGE_CODE",
|
|
environ_prefix=None,
|
|
)
|
|
|
|
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")),
|
|
)
|
|
)
|
|
|
|
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"),
|
|
os.path.join(
|
|
BASE_DIR, "admin", "templates"
|
|
), # enforce load before Django's admin
|
|
],
|
|
"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",
|
|
"mailbox_oauth2.middleware.one_time_email_authenticated_session",
|
|
"oauth2_provider.middleware.OAuth2TokenMiddleware",
|
|
"django.contrib.messages.middleware.MessageMiddleware",
|
|
"dockerflow.django.middleware.DockerflowMiddleware",
|
|
]
|
|
|
|
AUTHENTICATION_BACKENDS = [
|
|
"django.contrib.auth.backends.ModelBackend",
|
|
"mailbox_oauth2.backends.MailboxModelBackend",
|
|
"core.authentication.backends.OIDCAuthenticationBackend",
|
|
]
|
|
|
|
# Django's applications from the highest priority to the lowest
|
|
INSTALLED_PLUGINS = values.ListValue(
|
|
default=[],
|
|
environ_name="INSTALLED_PLUGINS",
|
|
environ_prefix=None,
|
|
)
|
|
INSTALLED_APPS = [
|
|
# People
|
|
"admin.apps.PeopleAdminConfig", # replaces 'django.contrib.admin'
|
|
"core",
|
|
"demo",
|
|
"mailbox_manager.apps.MailboxManagerConfig",
|
|
"mailbox_oauth2",
|
|
*INSTALLED_PLUGINS,
|
|
# Third party apps
|
|
"django_zxcvbn_password_validator",
|
|
"drf_spectacular",
|
|
"drf_spectacular_sidecar", # required for Django collectstatic discovery
|
|
"corsheaders",
|
|
"django_celery_beat",
|
|
"django_celery_results",
|
|
"dockerflow.django",
|
|
"easy_thumbnails",
|
|
"oauth2_provider",
|
|
"parler",
|
|
"rest_framework",
|
|
"treebeard",
|
|
# Django
|
|
"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.core.cache.backends.locmem.LocMemCache"},
|
|
}
|
|
|
|
REST_FRAMEWORK = {
|
|
"DEFAULT_AUTHENTICATION_CLASSES": (
|
|
# "core.resource_server.authentication.ResourceServerAuthentication",
|
|
# The resource server authentication is added on a per-view basis
|
|
# to enforce the filtering adapted from the introspected token.
|
|
# See ResourceServerMixin usage for more details.
|
|
"mozilla_django_oidc.contrib.drf.OIDCAuthentication",
|
|
"rest_framework.authentication.SessionAuthentication",
|
|
),
|
|
"DEFAULT_PARSER_CLASSES": [
|
|
"rest_framework.parsers.JSONParser",
|
|
"nested_multipart_parser.drf.DrfNestedParser",
|
|
],
|
|
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
|
|
"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_RATES": {
|
|
"sustained": values.Value(
|
|
default="150/hour",
|
|
environ_name="SUSTAINED_THROTTLE_RATES",
|
|
environ_prefix=None,
|
|
),
|
|
"burst": values.Value(
|
|
default="20/minute",
|
|
environ_name="BURST_THROTTLE_RATES",
|
|
environ_prefix=None,
|
|
),
|
|
},
|
|
}
|
|
|
|
SPECTACULAR_SETTINGS = {
|
|
"TITLE": "People API",
|
|
"DESCRIPTION": "This is the People 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",
|
|
}
|
|
|
|
# Django Admin
|
|
ADMIN_HEADER_BACKGROUND = values.Value(None)
|
|
ADMIN_HEADER_COLOR = values.Value(None)
|
|
|
|
# Mail
|
|
EMAIL_BACKEND = values.Value("django.core.mail.backends.smtp.EmailBackend")
|
|
EMAIL_HOST = values.Value(None)
|
|
EMAIL_HOST_USER = values.Value(None)
|
|
EMAIL_HOST_PASSWORD = values.Value(None)
|
|
EMAIL_PORT = values.PositiveIntegerValue(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")
|
|
|
|
# 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_RESULT_BACKEND = "django-db"
|
|
CELERY_CACHE_BACKEND = "django-cache"
|
|
CELERY_BROKER_TRANSPORT_OPTIONS = values.DictValue({})
|
|
CELERY_RESULT_EXTENDED = True
|
|
CELERY_TASK_RESULT_EXPIRES = 60 * 60 * 24 * 30 # 30 days
|
|
CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
|
|
|
|
# Session
|
|
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
|
|
SESSION_CACHE_ALIAS = "default"
|
|
SESSION_COOKIE_AGE = 60 * 60 * 12 # 12 hours to match Agent Connect
|
|
|
|
# Python loggers configuration (and env var overrides)
|
|
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": False,
|
|
},
|
|
},
|
|
}
|
|
|
|
# OIDC - Authorization Code Flow
|
|
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(
|
|
"people", environ_name="OIDC_RP_CLIENT_ID", environ_prefix=None
|
|
)
|
|
OIDC_RP_CLIENT_SECRET = values.Value(
|
|
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_INTROSPECTION_ENDPOINT = values.Value(
|
|
environ_name="OIDC_OP_INTROSPECTION_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_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
|
|
)
|
|
ALLOW_LOGOUT_GET_METHOD = values.BooleanValue(
|
|
default=True, environ_name="ALLOW_LOGOUT_GET_METHOD", 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,
|
|
)
|
|
OIDC_RS_ENCRYPTION_ALGO = values.Value(
|
|
default="RSA-OAEP",
|
|
environ_name="OIDC_RS_ENCRYPTION_ALGO",
|
|
environ_prefix=None,
|
|
)
|
|
OIDC_RS_ENCRYPTION_ENCODING = values.Value(
|
|
default="A256GCM",
|
|
environ_name="OIDC_RS_ENCRYPTION_ENCODING",
|
|
environ_prefix=None,
|
|
)
|
|
|
|
USER_OIDC_FIELDS_TO_NAME = values.ListValue(
|
|
default=["first_name", "last_name"],
|
|
environ_name="USER_OIDC_FIELDS_TO_NAME",
|
|
environ_prefix=None,
|
|
)
|
|
OIDC_ORGANIZATION_REGISTRATION_ID_FIELD = values.Value(
|
|
default=None,
|
|
environ_name="OIDC_ORGANIZATION_REGISTRATION_ID_FIELD",
|
|
environ_prefix=None,
|
|
)
|
|
|
|
OIDC_OP_TOKEN_INTROSPECTION_ENDPOINT = values.Value(
|
|
None, environ_name="OIDC_OP_TOKEN_INTROSPECTION_ENDPOINT", environ_prefix=None
|
|
)
|
|
OIDC_OP_URL = values.Value(None, environ_name="OIDC_OP_URL", environ_prefix=None)
|
|
OIDC_RS_BACKEND_CLASS = values.Value(
|
|
"core.resource_server.backend.ResourceServerBackend",
|
|
environ_name="OIDC_RS_BACKEND_CLASS",
|
|
environ_prefix=None,
|
|
)
|
|
OIDC_RS_AUDIENCE_CLAIM = values.Value(
|
|
"client_id",
|
|
environ_name="OIDC_RS_AUDIENCE_CLAIM",
|
|
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_SIGNING_ALGO = values.Value(
|
|
default="ES256", environ_name="OIDC_RS_SIGNING_ALGO", environ_prefix=None
|
|
)
|
|
OIDC_RS_SCOPES = values.ListValue(
|
|
["groups"], environ_name="OIDC_RS_SCOPES", environ_prefix=None
|
|
)
|
|
OIDC_PROXY = values.Value(None, environ_name="OIDC_PROXY", environ_prefix=None)
|
|
|
|
OIDC_VERIFY_SSL = values.BooleanValue(
|
|
True, environ_name="OIDC_VERIFY_SSL", environ_prefix=None
|
|
)
|
|
|
|
OIDC_TIMEOUT = values.Value(None, environ_name="OIDC_TIMEOUT", environ_prefix=None)
|
|
OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = values.BooleanValue(
|
|
default=True,
|
|
environ_name="OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION",
|
|
environ_prefix=None,
|
|
)
|
|
|
|
ACCOUNT_SERVICE_SCOPES = values.ListValue(
|
|
default=[],
|
|
environ_name="ACCOUNT_SERVICE_SCOPES",
|
|
environ_prefix=None,
|
|
)
|
|
|
|
# MAILBOX-PROVISIONING API
|
|
WEBMAIL_URL = values.Value(
|
|
default=None,
|
|
environ_name="WEBMAIL_URL",
|
|
environ_prefix=None,
|
|
)
|
|
MAIL_PROVISIONING_API_URL = values.Value(
|
|
default="http://dimail:8000",
|
|
environ_name="MAIL_PROVISIONING_API_URL",
|
|
environ_prefix=None,
|
|
)
|
|
MAIL_PROVISIONING_API_CREDENTIALS = values.Value(
|
|
default=None,
|
|
environ_name="MAIL_PROVISIONING_API_CREDENTIALS",
|
|
environ_prefix=None,
|
|
)
|
|
MAIL_PROVISIONING_API_TIMEOUT = values.IntegerValue(
|
|
default=20,
|
|
environ_name="MAIL_PROVISIONING_API_TIMEOUT",
|
|
environ_prefix=None,
|
|
)
|
|
MAIL_CHECK_DOMAIN_INTERVAL = values.IntegerValue(
|
|
default=10,
|
|
environ_name="MAIL_CHECK_DOMAIN_INTERVAL",
|
|
environ_prefix=None,
|
|
)
|
|
DNS_PROVISIONING_TARGET_ZONE = values.Value(
|
|
default=None,
|
|
environ_name="DNS_PROVISIONING_TARGET_ZONE",
|
|
environ_prefix=None,
|
|
)
|
|
DNS_PROVISIONING_API_URL = values.Value(
|
|
default="https://api.scaleway.com",
|
|
environ_name="DNS_PROVISIONING_API_URL",
|
|
environ_prefix=None,
|
|
)
|
|
DNS_PROVISIONING_RESOURCE_ID = values.Value(
|
|
default=None,
|
|
environ_name="DNS_PROVISIONING_RESOURCE_ID",
|
|
environ_prefix=None,
|
|
)
|
|
DNS_PROVISIONING_API_CREDENTIALS = values.Value(
|
|
default=None,
|
|
environ_name="DNS_PROVISIONING_API_CREDENTIALS",
|
|
environ_prefix=None,
|
|
)
|
|
|
|
# Organizations
|
|
ORGANIZATION_REGISTRATION_ID_VALIDATORS = json.loads(
|
|
values.Value(
|
|
default="[]",
|
|
environ_name="ORGANIZATION_REGISTRATION_ID_VALIDATORS",
|
|
environ_prefix=None,
|
|
)
|
|
)
|
|
ORGANIZATION_METADATA_SCHEMA = values.Value(
|
|
default=None,
|
|
environ_name="ORGANIZATION_METADATA_SCHEMA",
|
|
environ_prefix=None,
|
|
)
|
|
|
|
OAUTH2_PROVIDER_APPLICATION_MODEL = "oauth2_provider.Application"
|
|
OAUTH2_PROVIDER_GRANT_MODEL = "mailbox_oauth2.Grant"
|
|
OAUTH2_PROVIDER_ID_TOKEN_MODEL = "mailbox_oauth2.IDToken" # noqa: S105
|
|
OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = "mailbox_oauth2.AccessToken" # noqa: S105
|
|
OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "mailbox_oauth2.RefreshToken" # noqa: S105
|
|
|
|
# Security settings for login attempts
|
|
# - Maximum number of failed login attempts before lockout
|
|
MAX_LOGIN_ATTEMPTS = values.IntegerValue(
|
|
default=5,
|
|
environ_name="MAX_LOGIN_ATTEMPTS",
|
|
environ_prefix=None,
|
|
)
|
|
# - Lockout time in seconds (default to 5 minutes)
|
|
ACCOUNT_LOCKOUT_TIME = values.IntegerValue(
|
|
default=5 * 60,
|
|
environ_name="ACCOUNT_LOCKOUT_TIME",
|
|
environ_prefix=None,
|
|
)
|
|
|
|
MANAGEMENT_COMMAND_AS_TASK = [
|
|
"fill_organization_metadata",
|
|
] + values.ListValue(
|
|
default=[],
|
|
environ_name="MANAGEMENT_COMMAND_AS_TASK",
|
|
environ_prefix=None,
|
|
)
|
|
|
|
# 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 FEATURES(self):
|
|
"""Feature flags for the application."""
|
|
FEATURE_FLAGS: set = {
|
|
"CONTACTS_CREATE", # Used in the users/me/ endpoint
|
|
"CONTACTS_DISPLAY", # Used in the users/me/ endpoint
|
|
"MAILBOXES_CREATE", # Used in the users/me/ endpoint
|
|
"TEAMS_DISPLAY",
|
|
"TEAMS_CREATE", # Used in the users/me/ endpoint
|
|
}
|
|
|
|
return {
|
|
feature: values.BooleanValue(
|
|
default=True,
|
|
environ_name=f"FEATURE_{feature}",
|
|
environ_prefix=None,
|
|
)
|
|
for feature in FEATURE_FLAGS
|
|
}
|
|
|
|
# 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 COMMIT(self):
|
|
"""
|
|
Return the commit information.
|
|
"""
|
|
return get_commit()
|
|
|
|
# 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,
|
|
},
|
|
}
|
|
|
|
@property
|
|
def OAUTH2_PROVIDER(self) -> dict:
|
|
"""OAuth2 Provider settings."""
|
|
OIDC_ENABLED = values.BooleanValue(
|
|
default=False,
|
|
environ_name="OAUTH2_PROVIDER_OIDC_ENABLED",
|
|
environ_prefix=None,
|
|
)
|
|
OIDC_RSA_PRIVATE_KEY = values.Value(
|
|
environ_name="OAUTH2_PROVIDER_OIDC_RSA_PRIVATE_KEY",
|
|
environ_prefix=None,
|
|
)
|
|
OAUTH2_VALIDATOR_CLASS = values.Value(
|
|
default="mailbox_oauth2.validators.BaseValidator",
|
|
environ_name="OAUTH2_PROVIDER_VALIDATOR_CLASS",
|
|
environ_prefix=None,
|
|
)
|
|
SCOPES = {
|
|
"openid": "OpenID Connect scope",
|
|
"email": "Email address",
|
|
}
|
|
if OAUTH2_VALIDATOR_CLASS == "mailbox_oauth2.validators.ProConnectValidator":
|
|
SCOPES["given_name"] = "First name"
|
|
SCOPES["usual_name"] = "Last name"
|
|
SCOPES["siret"] = "SIRET number"
|
|
SCOPES["siren"] = "SIREN number"
|
|
SCOPES["uid"] = "UID"
|
|
# available but not filled
|
|
SCOPES["organizational_unit"] = "Organizational unit"
|
|
SCOPES["belonging_population"] = "Belonging population"
|
|
SCOPES["phone"] = "Phone number"
|
|
SCOPES["chorusdt"] = "Chorus DT"
|
|
|
|
return {
|
|
"OIDC_ENABLED": OIDC_ENABLED,
|
|
"OIDC_RSA_PRIVATE_KEY": OIDC_RSA_PRIVATE_KEY,
|
|
"SCOPES": SCOPES,
|
|
"OAUTH2_VALIDATOR_CLASS": OAUTH2_VALIDATOR_CLASS,
|
|
}
|
|
|
|
@property
|
|
def LOGIN_URL(self):
|
|
"""
|
|
Define the LOGIN_URL (Django) for the OIDC provider (reuse LOGIN_REDIRECT_URL)
|
|
"""
|
|
return f"{self.LOGIN_REDIRECT_URL}/login/"
|
|
|
|
@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()],
|
|
traces_sample_rate=0.1,
|
|
)
|
|
|
|
# Add the application name to the Sentry scope
|
|
scope = sentry_sdk.get_global_scope()
|
|
scope.set_tag("application", "backend")
|
|
|
|
# Ignore the logs added by the DockerflowMiddleware
|
|
ignore_logger("request.summary")
|
|
|
|
@classmethod
|
|
def generate_temporary_rsa_key(cls):
|
|
"""Generate a temporary RSA key for OIDC Provider."""
|
|
|
|
private_key = rsa.generate_private_key(
|
|
public_exponent=65537,
|
|
key_size=4096,
|
|
)
|
|
|
|
# - Serialize private key to PEM format
|
|
private_key_pem = private_key.private_bytes(
|
|
encoding=serialization.Encoding.PEM,
|
|
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
encryption_algorithm=serialization.NoEncryption(),
|
|
)
|
|
|
|
return private_key_pem.decode("utf-8")
|
|
|
|
|
|
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 = ["http://localhost:8072", "http://localhost:3000"]
|
|
DEBUG = True
|
|
|
|
SESSION_COOKIE_NAME = "people_sessionid"
|
|
|
|
# this is a dev credentials for mail provisioning API
|
|
MAIL_PROVISIONING_API_CREDENTIALS = "bGFfcmVnaWU6cGFzc3dvcmQ="
|
|
|
|
OIDC_ORGANIZATION_REGISTRATION_ID_FIELD = "siret"
|
|
|
|
def __init__(self):
|
|
"""In dev, force installs needed for Swagger API."""
|
|
# pylint: disable=invalid-name
|
|
self.INSTALLED_APPS += ["django_extensions"]
|
|
|
|
@property
|
|
def OAUTH2_PROVIDER(self):
|
|
"""OAuth2 Provider settings."""
|
|
OAUTH2_PROVIDER = super().OAUTH2_PROVIDER # pylint: disable=invalid-name
|
|
if not OAUTH2_PROVIDER["OIDC_RSA_PRIVATE_KEY"]:
|
|
OAUTH2_PROVIDER["OIDC_RSA_PRIVATE_KEY"] = Base.generate_temporary_rsa_key()
|
|
return OAUTH2_PROVIDER
|
|
|
|
|
|
class Test(Base):
|
|
"""Test environment settings"""
|
|
|
|
LOGGING = values.DictValue(
|
|
{
|
|
"version": 1,
|
|
"disable_existing_loggers": False,
|
|
"handlers": {
|
|
"console": {
|
|
"class": "logging.StreamHandler",
|
|
},
|
|
},
|
|
"loggers": {
|
|
"people": {
|
|
"handlers": ["console"],
|
|
"level": "DEBUG",
|
|
},
|
|
},
|
|
}
|
|
)
|
|
PASSWORD_HASHERS = [
|
|
"django.contrib.auth.hashers.MD5PasswordHasher",
|
|
]
|
|
USE_SWAGGER = True
|
|
|
|
STORAGES = {
|
|
"default": {
|
|
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
|
},
|
|
"staticfiles": {
|
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
|
},
|
|
}
|
|
|
|
CELERY_TASK_ALWAYS_EAGER = values.BooleanValue(True)
|
|
|
|
# this is a dev credentials for mail provisioning API
|
|
MAIL_PROVISIONING_API_CREDENTIALS = "bGFfcmVnaWU6cGFzc3dvcmQ="
|
|
|
|
OIDC_ORGANIZATION_REGISTRATION_ID_FIELD = "siret"
|
|
|
|
ORGANIZATION_REGISTRATION_ID_VALIDATORS = [
|
|
{
|
|
"NAME": "django.core.validators.RegexValidator",
|
|
"OPTIONS": {
|
|
"regex": "^[0-9]{14}$",
|
|
},
|
|
},
|
|
]
|
|
|
|
|
|
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
|
|
ALLOWED_HOSTS = values.ListValue(None)
|
|
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_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
|
|
|
|
# Password management
|
|
|
|
# - Password strength for ZxcvbnPasswordValidator
|
|
# 0 too guessable: risky password. (guesses < 10^3)
|
|
# 1 very guessable: protection from throttled online attacks. (guesses < 10^6)
|
|
# 2 somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8)
|
|
# 3 safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10)
|
|
# 4 very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10)
|
|
PASSWORD_MINIMAL_STRENGTH = values.IntegerValue(
|
|
default=3,
|
|
environ_name="PASSWORD_MINIMAL_STRENGTH",
|
|
environ_prefix=None,
|
|
)
|
|
|
|
AUTH_PASSWORD_VALIDATORS = [
|
|
{
|
|
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
|
"OPTIONS": {
|
|
"user_attributes": (
|
|
"email", # for core.User
|
|
"name", # for core.User
|
|
"first_name", # for mailbox_manager.Mailbox
|
|
"last_name", # for mailbox_manager.Mailbox
|
|
"local_part", # for mailbox_manager.Mailbox
|
|
),
|
|
},
|
|
},
|
|
{
|
|
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
|
},
|
|
{
|
|
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
|
},
|
|
{
|
|
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
|
},
|
|
{
|
|
"NAME": "django_zxcvbn_password_validator.ZxcvbnPasswordValidator",
|
|
},
|
|
]
|
|
|
|
# For static files in production, we want to use a backend that includes a hash in
|
|
# the filename, that is calculated from the file content, so that browsers always
|
|
# get the updated version of each file.
|
|
STORAGES = {
|
|
"default": {
|
|
"BACKEND": "storages.backends.s3.S3Storage",
|
|
},
|
|
"staticfiles": {
|
|
# For static files in production, we want to use a backend that includes a hash in
|
|
# the filename, that is calculated from the file content, so that browsers always
|
|
# get the updated version of each file.
|
|
"BACKEND": values.Value(
|
|
"whitenoise.storage.CompressedManifestStaticFilesStorage",
|
|
environ_name="STORAGES_STATICFILES_BACKEND",
|
|
)
|
|
},
|
|
}
|
|
|
|
# Privacy
|
|
SECURE_REFERRER_POLICY = "same-origin"
|
|
|
|
# Media
|
|
AWS_S3_ENDPOINT_URL = values.Value()
|
|
AWS_S3_ACCESS_KEY_ID = values.Value()
|
|
AWS_S3_SECRET_ACCESS_KEY = values.Value()
|
|
AWS_STORAGE_BUCKET_NAME = values.Value("tf-default-people-media-storage")
|
|
AWS_S3_REGION_NAME = values.Value()
|
|
|
|
CACHES = {
|
|
"default": {
|
|
"BACKEND": "django_redis.cache.RedisCache",
|
|
"LOCATION": values.Value(
|
|
"redis://redis:6379/1",
|
|
environ_name="REDIS_URL",
|
|
environ_prefix=None,
|
|
),
|
|
"OPTIONS": {
|
|
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
|
},
|
|
},
|
|
}
|
|
SENTRY_DSN = values.Value(
|
|
"https://b72746c73d669421e7a8ccd3fab0fad2@sentry.incubateur.net/171",
|
|
environ_name="SENTRY_DSN",
|
|
)
|
|
|
|
|
|
class Feature(Production):
|
|
"""
|
|
Feature environment settings
|
|
|
|
nota bene: it should inherit from the Production environment.
|
|
"""
|
|
|
|
|
|
class Local(Production):
|
|
"""
|
|
Local environment settings
|
|
|
|
This configuration is used by the developers to run the application
|
|
locally through the helm files (used for tilt)
|
|
|
|
nota bene: it should inherit from the Production environment.
|
|
"""
|
|
|
|
@property
|
|
def OAUTH2_PROVIDER(self):
|
|
"""OAuth2 Provider settings."""
|
|
OAUTH2_PROVIDER = super().OAUTH2_PROVIDER # pylint: disable=invalid-name
|
|
if not OAUTH2_PROVIDER["OIDC_RSA_PRIVATE_KEY"]:
|
|
OAUTH2_PROVIDER["OIDC_RSA_PRIVATE_KEY"] = Base.generate_temporary_rsa_key()
|
|
return OAUTH2_PROVIDER
|
|
|
|
|
|
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",
|
|
},
|
|
}
|