forked from ungleich-public/ungleich-user
It opens with the five required variables, then what production usually sets (proxy header, PostgreSQL, mail relay, OTP), then every optional setting commented out at its default. It lists exactly the 51 variables the code reads.
278 lines
9.7 KiB
Python
278 lines
9.7 KiB
Python
"""
|
|
Django settings for the ungleich account webapp (dal).
|
|
|
|
All deployment-specific values come from environment variables (or a
|
|
.env file in development), read here and nowhere else. See .env.example
|
|
and docker.md for the full list, which ones are required, and which are
|
|
secrets.
|
|
"""
|
|
|
|
import os
|
|
from decouple import config, Csv
|
|
|
|
# LDAP setup
|
|
# Space separated list of LDAP URIs; the first reachable one is used.
|
|
LDAP_SERVERS = config('LDAPSERVER').split()
|
|
|
|
LDAP_ADMIN_DN = config('LDAP_ADMIN_DN')
|
|
LDAP_ADMIN_PASSWORD = config('LDAP_ADMIN_PASSWORD')
|
|
|
|
# The directory layout defaults to production's: customers in ou=customer,
|
|
# staff in ou=users, both below one base. Another tree (e.g. a test
|
|
# server) usually only needs ENTIRE_SEARCH_BASE.
|
|
# Usernames and uidNumbers are unique below ENTIRE_SEARCH_BASE.
|
|
ENTIRE_SEARCH_BASE = config('ENTIRE_SEARCH_BASE', default='dc=ungleich,dc=ch')
|
|
# New registrations are created here.
|
|
LDAP_CUSTOMER_DN = config('LDAP_CUSTOMER_DN', default='ou=customer,' + ENTIRE_SEARCH_BASE)
|
|
LDAP_CUSTOMER_GROUP_ID = config('LDAP_CUSTOMER_GROUP_ID', default=10004, cast=int)
|
|
|
|
LDAP_DEFAULT_START_UID = config('LDAP_DEFAULT_START_UID', default=10000, cast=int)
|
|
# objectClasses of new customer entries (space separated). ldapPublicKey
|
|
# needs the openssh-lpk schema, which production has.
|
|
LDAP_USER_OBJECT_CLASSES = config(
|
|
'LDAP_USER_OBJECT_CLASSES', default='inetOrgPerson posixAccount ldapPublicKey').split()
|
|
|
|
# Users log in from any of these (space separated) search bases.
|
|
LDAP_SEARCH_BASES = config(
|
|
'LDAPSEARCH',
|
|
default='ou=customer,{0} ou=users,{0}'.format(ENTIRE_SEARCH_BASE)).split()
|
|
|
|
# StartTLS on every connection (use ldaps:// URIs for implicit TLS).
|
|
LDAP_USE_TLS = config('LDAP_USE_TLS', default=False, cast=bool)
|
|
# Verify LDAP server certificates (ldaps:// and StartTLS). LDAP_CA_FILE
|
|
# adds a CA bundle for servers with a private CA; the system store is used
|
|
# otherwise. Only disable verification for local experiments.
|
|
LDAP_TLS_VERIFY = config('LDAP_TLS_VERIFY', default=True, cast=bool)
|
|
LDAP_CA_FILE = config('LDAP_CA_FILE', default='')
|
|
|
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
DEBUG = config('DEBUG', default=False, cast=bool)
|
|
|
|
EMAIL_FROM_ADDRESS = config('EMAIL_FROM_ADDRESS')
|
|
|
|
MAILERS = {
|
|
"default": {
|
|
"BACKEND": "django.core.mail.backends.smtp.EmailBackend",
|
|
"OPTIONS": {
|
|
"host": config("EMAIL_HOST", default="localhost"),
|
|
"port": config("EMAIL_PORT", default=25, cast=int),
|
|
"username": config("EMAIL_HOST_USER", default=""),
|
|
"password": config("EMAIL_HOST_PASSWORD", default=""),
|
|
"use_tls": config("EMAIL_USE_TLS", default=True, cast=bool),
|
|
"timeout": config("EMAIL_TIMEOUT", default=10, cast=int),
|
|
},
|
|
},
|
|
}
|
|
|
|
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='account.ungleich.ch', cast=Csv())
|
|
|
|
# --- HTTPS / proxy -----------------------------------------------------------
|
|
# Behind a TLS-terminating proxy (e.g. a Kubernetes ingress) Django sees plain
|
|
# HTTP. Only enable this if the proxy always sets X-Forwarded-Proto.
|
|
if config('TRUST_X_FORWARDED_PROTO', default=False, cast=bool):
|
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
|
|
|
# Full origins (scheme included). Defaults to https:// plus each
|
|
# ALLOWED_HOSTS entry, which is what a site behind an HTTPS ingress needs.
|
|
CSRF_TRUSTED_ORIGINS = config(
|
|
'CSRF_TRUSTED_ORIGINS',
|
|
default=','.join('https://' + h.lstrip('.') for h in ALLOWED_HOSTS if h != '*'),
|
|
cast=Csv())
|
|
|
|
SESSION_COOKIE_SECURE = config('SESSION_COOKIE_SECURE', default=not DEBUG, cast=bool)
|
|
CSRF_COOKIE_SECURE = config('CSRF_COOKIE_SECURE', default=not DEBUG, cast=bool)
|
|
SECURE_SSL_REDIRECT = config('SECURE_SSL_REDIRECT', default=False, cast=bool)
|
|
SECURE_HSTS_SECONDS = config('SECURE_HSTS_SECONDS', default=0, cast=int)
|
|
|
|
INSTALLED_APPS = [
|
|
'django.contrib.admin',
|
|
'django.contrib.auth',
|
|
'django.contrib.contenttypes',
|
|
'django.contrib.sessions',
|
|
'django.contrib.messages',
|
|
'django.contrib.staticfiles',
|
|
'bootstrap3',
|
|
'dal',
|
|
'rest_framework'
|
|
]
|
|
|
|
MIDDLEWARE = [
|
|
# First: answers /healthz and /readyz before host checks and redirects.
|
|
'dal.health.HealthCheckMiddleware',
|
|
'django.middleware.security.SecurityMiddleware',
|
|
'whitenoise.middleware.WhiteNoiseMiddleware',
|
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
|
'django.middleware.common.CommonMiddleware',
|
|
'django.middleware.csrf.CsrfViewMiddleware',
|
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
|
'django.contrib.messages.middleware.MessageMiddleware',
|
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
]
|
|
|
|
AUTHENTICATION_BACKENDS = (
|
|
'dal.backends.LdapBackend',
|
|
)
|
|
|
|
|
|
ROOT_URLCONF = 'dal.urls'
|
|
|
|
TEMPLATES = [
|
|
{
|
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
|
'DIRS': [],
|
|
'APP_DIRS': True,
|
|
'OPTIONS': {
|
|
'context_processors': [
|
|
'django.template.context_processors.debug',
|
|
'django.template.context_processors.request',
|
|
'django.contrib.auth.context_processors.auth',
|
|
'django.contrib.messages.context_processors.messages',
|
|
],
|
|
},
|
|
},
|
|
]
|
|
|
|
WSGI_APPLICATION = 'dal.wsgi.application'
|
|
|
|
# Password validation
|
|
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
|
|
|
|
AUTH_PASSWORD_VALIDATORS = [
|
|
{
|
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
|
},
|
|
{
|
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
|
},
|
|
{
|
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
|
},
|
|
{
|
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
|
},
|
|
]
|
|
|
|
|
|
# Internationalization
|
|
# https://docs.djangoproject.com/en/1.10/topics/i18n/
|
|
|
|
LANGUAGE_CODE = 'en-us'
|
|
|
|
TIME_ZONE = 'UTC'
|
|
|
|
USE_I18N = True
|
|
|
|
USE_TZ = True
|
|
|
|
|
|
# Static files (CSS, JavaScript, Images)
|
|
# https://docs.djangoproject.com/en/1.10/howto/static-files/
|
|
|
|
STATIC_URL = '/static/'
|
|
# collectstatic target; served by WhiteNoise (gzip compressed).
|
|
# Not the hashed "Manifest" storage: the bundled vendor CSS references
|
|
# source maps that are not shipped, which makes hashing fail.
|
|
STATIC_ROOT = config('STATIC_ROOT', default=os.path.join(BASE_DIR, 'static'))
|
|
|
|
STORAGES = {
|
|
'default': {'BACKEND': 'django.core.files.storage.FileSystemStorage'},
|
|
'staticfiles': {
|
|
'BACKEND': 'whitenoise.storage.CompressedStaticFilesStorage',
|
|
},
|
|
}
|
|
|
|
# Database
|
|
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
|
|
|
|
# PostgreSQL when DATABASE_HOST is set (needed for more than one replica),
|
|
# otherwise SQLite at SQLITE_PATH.
|
|
if config('DATABASE_HOST', default=''):
|
|
DATABASES = {
|
|
'default': {
|
|
'ENGINE': 'django.db.backends.postgresql',
|
|
'HOST': config('DATABASE_HOST'),
|
|
'PORT': config('DATABASE_PORT', default=5432, cast=int),
|
|
'NAME': config('DATABASE_NAME', default='ungleich_user'),
|
|
'USER': config('DATABASE_USER', default='ungleich_user'),
|
|
'PASSWORD': config('DATABASE_PASSWORD', default=''),
|
|
'CONN_MAX_AGE': config('DATABASE_CONN_MAX_AGE', default=60, cast=int),
|
|
'CONN_HEALTH_CHECKS': True,
|
|
'OPTIONS': {
|
|
'sslmode': config('DATABASE_SSLMODE', default='prefer'),
|
|
},
|
|
}
|
|
}
|
|
else:
|
|
DATABASES = {
|
|
'default': {
|
|
'ENGINE': 'django.db.backends.sqlite3',
|
|
'NAME': config('SQLITE_PATH', default=os.path.join(BASE_DIR, 'db.sqlite3')),
|
|
}
|
|
}
|
|
SECRET_KEY = config('SECRET_KEY')
|
|
|
|
# Keep 32-bit ids: existing tables were created with AutoField.
|
|
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
|
|
|
AUTH_LDAP_USER_ATTR_MAP = {
|
|
"first_name": "givenName",
|
|
"last_name": "sn",
|
|
"email": "mail"
|
|
}
|
|
|
|
# Everything goes to stdout/stderr (12-factor / Kubernetes friendly).
|
|
LOG_LEVEL = config('LOG_LEVEL', default='INFO').upper()
|
|
|
|
LOGGING = {
|
|
'version': 1,
|
|
'disable_existing_loggers': False,
|
|
'formatters': {
|
|
'standard': {
|
|
'format': '%(asctime)s %(levelname)s %(name)s %(funcName)s %(lineno)d: %(message)s'
|
|
}
|
|
},
|
|
'handlers': {
|
|
'console': {
|
|
'class': 'logging.StreamHandler',
|
|
'formatter': 'standard',
|
|
},
|
|
},
|
|
'root': {
|
|
'handlers': ['console'],
|
|
'level': LOG_LEVEL,
|
|
},
|
|
'loggers': {
|
|
# Keep Django's own request/DB chatter at INFO unless asked for.
|
|
'django': {
|
|
'level': config('DJANGO_LOG_LEVEL', default='INFO').upper(),
|
|
},
|
|
},
|
|
}
|
|
|
|
# ldap3 protocol logging is very verbose; opt in with LDAP3_LOG=1.
|
|
if config('LDAP3_LOG', default=False, cast=bool):
|
|
from ldap3.utils.log import set_library_log_detail_level, BASIC
|
|
set_library_log_detail_level(BASIC)
|
|
|
|
|
|
# OTP server that stores per-user TOTP seeds, accessed with an admin TOTP.
|
|
# Optional: without OTPSERVER, new accounts get no OTP seed and the seed
|
|
# pages report that OTP is not configured.
|
|
OTP_SERVER = config('OTPSERVER', default='')
|
|
OTP_ADMIN_SEED = config('ADMIN_SEED', default='')
|
|
OTP_ADMIN_NAME = config('ADMIN_NAME', default='')
|
|
OTP_ADMIN_REALM = config('ADMIN_REALM', default='')
|
|
OTP_USER_REALM = config('USER_REALM', default='')
|
|
OTP_ALLOWED_REALMS = config('ALLOWED_REALMS', default='', cast=Csv())
|
|
if OTP_SERVER and not all([OTP_ADMIN_SEED, OTP_ADMIN_NAME, OTP_ADMIN_REALM, OTP_USER_REALM]):
|
|
from django.core.exceptions import ImproperlyConfigured
|
|
raise ImproperlyConfigured(
|
|
'OTPSERVER is set, so ADMIN_SEED, ADMIN_NAME, ADMIN_REALM and USER_REALM are required too')
|
|
OTP_TIMEOUT = config('OTP_TIMEOUT', default=10, cast=int) # seconds
|
|
|
|
REST_FRAMEWORK = {
|
|
'DEFAULT_RENDERER_CLASSES': (
|
|
'rest_framework.renderers.JSONRenderer',
|
|
)
|
|
}
|