Password page with the AD policy listed first, ordered service (throttle limit below the lockout threshold, verification, local hints, directory change, ended sessions, audit and confirmation mail in one transaction), plain-language errors, German text and tests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N3t4WL7bd9bdHHNjEsxzqC
133 lines
4.3 KiB
Python
133 lines
4.3 KiB
Python
"""Django settings. All deployment values come from environment variables (AD-13)."""
|
|
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from .env import ConfigError, load_config
|
|
from .logging import build_logging
|
|
|
|
try:
|
|
_config = load_config(os.environ)
|
|
except ConfigError as exc:
|
|
# One clear message, no traceback, no values.
|
|
print(str(exc), file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
|
|
|
SECRET_KEY = _config.secret_key
|
|
DEBUG = _config.debug
|
|
ALLOWED_HOSTS = _config.allowed_hosts
|
|
# The client-IP rule (AD-7) trusts X-Forwarded-For only from these peers.
|
|
TRUSTED_PROXIES = _config.trusted_proxies
|
|
DATA_DIR = _config.data_dir
|
|
DISPLAY_TIME_ZONE = _config.default_timezone
|
|
DIRECTORY = _config.directory
|
|
OIDC = _config.oidc
|
|
SMTP = _config.smtp
|
|
|
|
INSTALLED_APPS = [
|
|
"django.contrib.contenttypes",
|
|
"django.contrib.auth",
|
|
"django.contrib.sessions",
|
|
"django.contrib.staticfiles",
|
|
"portal.audit",
|
|
"portal.accounts",
|
|
"portal.shell",
|
|
"portal.profile",
|
|
"portal.mailer",
|
|
"portal.scheduler",
|
|
]
|
|
|
|
MIDDLEWARE = [
|
|
"portal.config.middleware.RequestIdMiddleware",
|
|
"portal.accounts.middleware.ClientIpMiddleware",
|
|
"portal.config.middleware.SecurityHeadersMiddleware",
|
|
"django.middleware.security.SecurityMiddleware",
|
|
"whitenoise.middleware.WhiteNoiseMiddleware",
|
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
|
"portal.accounts.middleware.AbsoluteSessionLimitMiddleware",
|
|
"portal.accounts.middleware.CurrentUserMiddleware",
|
|
"portal.shell.middleware.PreferencesMiddleware",
|
|
"django.middleware.common.CommonMiddleware",
|
|
"django.middleware.csrf.CsrfViewMiddleware",
|
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
|
]
|
|
|
|
ROOT_URLCONF = "portal.config.urls"
|
|
WSGI_APPLICATION = "portal.config.wsgi.application"
|
|
|
|
TEMPLATES = [
|
|
{
|
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
|
"DIRS": [BASE_DIR / "portal" / "templates"],
|
|
"APP_DIRS": True,
|
|
"OPTIONS": {
|
|
"context_processors": [
|
|
"django.template.context_processors.request",
|
|
"django.template.context_processors.i18n",
|
|
"portal.shell.context.shell",
|
|
],
|
|
},
|
|
}
|
|
]
|
|
|
|
DATABASES = {
|
|
"default": {
|
|
"ENGINE": "django.db.backends.sqlite3",
|
|
"NAME": DATA_DIR / "db.sqlite3",
|
|
"OPTIONS": {
|
|
"transaction_mode": "IMMEDIATE",
|
|
"timeout": 20,
|
|
"init_command": "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;",
|
|
},
|
|
}
|
|
}
|
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
|
|
|
# Time and language (AD-15): store UTC, translate every string.
|
|
USE_TZ = True
|
|
TIME_ZONE = "UTC"
|
|
USE_I18N = True
|
|
LANGUAGE_CODE = "en"
|
|
LANGUAGES = [("en", "English"), ("de", "Deutsch")]
|
|
LOCALE_PATHS = [BASE_DIR / "portal" / "locale"]
|
|
|
|
# E-mails use the portal-wide language and form of address (a per-device choice cannot reach
|
|
# server-sent mail). Becomes a runtime setting with the settings registry.
|
|
MAIL_LANGUAGE = "de"
|
|
|
|
# Static files served by whitenoise; assets are built into the image.
|
|
STATIC_URL = "static/"
|
|
STATIC_ROOT = BASE_DIR / "staticfiles"
|
|
STATICFILES_DIRS = [BASE_DIR / "portal" / "static"]
|
|
STORAGES = {
|
|
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
|
"staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
|
|
}
|
|
|
|
# Sessions in the database; cookies secure, HttpOnly, SameSite=Lax (AD-6).
|
|
SESSION_ENGINE = "portal.accounts.session_store"
|
|
SESSION_COOKIE_AGE = 60 * 60 # idle limit: 1 hour, renewed on every request
|
|
SESSION_SAVE_EVERY_REQUEST = True
|
|
SESSION_ABSOLUTE_SECONDS = 4 * 60 * 60 # absolute limit: 4 hours
|
|
# How long roles and account state are trusted before the directory is asked again (AD-5).
|
|
ROLE_CACHE_SECONDS = 60
|
|
SESSION_COOKIE_SECURE = not DEBUG
|
|
SESSION_COOKIE_HTTPONLY = True
|
|
SESSION_COOKIE_SAMESITE = "Lax"
|
|
CSRF_COOKIE_SECURE = not DEBUG
|
|
CSRF_TRUSTED_ORIGINS = [OIDC.public_base_url]
|
|
CSRF_COOKIE_HTTPONLY = True
|
|
CSRF_COOKIE_SAMESITE = "Lax"
|
|
|
|
# TLS ends at the reverse proxy.
|
|
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
|
SECURE_CONTENT_TYPE_NOSNIFF = True
|
|
SECURE_REFERRER_POLICY = "same-origin"
|
|
SECURE_HSTS_SECONDS = 0 if DEBUG else 31536000
|
|
X_FRAME_OPTIONS = "DENY"
|
|
|
|
LOGGING = build_logging(_config.log_level)
|