Files
training-software/config/settings/base.py
Paperclip CTO 1e886f2022
Some checks failed
CI / lint (push) Failing after 8s
CI / test (push) Failing after 8s
CI / build-container (push) Has been skipped
fix(TRA-379): generate missing migrations and fix daphne INSTALLED_APPS ordering
- Move daphne before django.contrib.staticfiles in INSTALLED_APPS so Daphne
  system check passes and makemigrations --check can run cleanly
- Generate tracking/0002: rename auto-generated AuditEvent index names to
  match Django 5.2 format
- Generate accounts/0005: sync AccountUser managers and ManyToMany field
  help_text to match current Django/guardian defaults

The CallSession / CallParticipant / CallEvent migration (training/0004)
already existed from the groupcall backend commit; this commit ensures
makemigrations --check reports no pending migrations across all apps.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-18 14:44:51 +02:00

284 lines
7.8 KiB
Python

from datetime import timedelta
import environ
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent.parent
env = environ.Env()
SECRET_KEY = env("DJANGO_SECRET_KEY")
DEBUG = False
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=[])
DJANGO_APPS = [
"daphne",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]
THIRD_PARTY_APPS = [
"channels",
"rest_framework",
"rest_framework_simplejwt",
"django_filters",
"drf_spectacular",
"mozilla_django_oidc",
"guardian",
"django_celery_results",
"django_celery_beat",
]
LOCAL_APPS = [
"api",
"accounts",
"courses",
"cms",
"tracking",
"quizzes",
"training",
"certificates",
"reports",
"notifications",
"waitlist",
"attendance",
]
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
AUTH_USER_MODEL = "accounts.AccountUser"
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"csp.middleware.CSPMiddleware",
"api.middleware.SecurityAuditMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "config.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"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 = "config.wsgi.application"
ASGI_APPLICATION = "config.asgi.application"
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [env("REDIS_URL", default="redis://localhost:6379/0")],
},
},
}
DATABASES = {
"default": env.db("DATABASE_URL"),
}
CACHES = {
"default": env.cache("REDIS_URL"),
}
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"},
]
AUTHENTICATION_BACKENDS = [
"accounts.oidc_backend.TrainingOIDCBackend",
"django.contrib.auth.backends.ModelBackend",
"guardian.backends.ObjectPermissionBackend",
]
LANGUAGE_CODE = "de-de"
LANGUAGES = [
("de", "Deutsch"),
("en", "English"),
]
LOCALE_PATHS = [BASE_DIR / "locale"]
TIME_ZONE = "Europe/Berlin"
USE_I18N = True
USE_L10N = True
USE_TZ = True
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "mediafiles"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# --- DRF ---
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
"rest_framework.authentication.SessionAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 50,
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend",
"rest_framework.filters.OrderingFilter",
"rest_framework.filters.SearchFilter",
],
"EXCEPTION_HANDLER": "api.exceptions.custom_exception_handler",
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}
# --- drf-spectacular ---
SPECTACULAR_SETTINGS = {
"TITLE": "Trainingssoftware API",
"VERSION": "1.0.0",
"SERVE_INCLUDE_SCHEMA": False,
"SCHEMA_PATH_PREFIX": r"/api/v[0-9]+",
"COMPONENT_SPLIT_REQUEST": True,
"POSTPROCESSING_HOOKS": [
"drf_spectacular.hooks.postprocess_schema_enums",
],
}
# --- Celery ---
CELERY_BROKER_URL = env("REDIS_URL")
CELERY_RESULT_BACKEND = "django-db"
CELERY_CACHE_BACKEND = "default"
CELERY_ACCEPT_CONTENT = ["json"]
CELERY_TASK_SERIALIZER = "json"
CELERY_RESULT_SERIALIZER = "json"
CELERY_TIMEZONE = TIME_ZONE
CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
# --- OIDC ---
OIDC_RP_CLIENT_ID = env("OIDC_RP_CLIENT_ID", default="")
OIDC_RP_CLIENT_SECRET = env("OIDC_RP_CLIENT_SECRET", default="")
OIDC_OP_AUTHORIZATION_ENDPOINT = env("OIDC_OP_AUTHORIZATION_ENDPOINT", default="")
OIDC_OP_TOKEN_ENDPOINT = env("OIDC_OP_TOKEN_ENDPOINT", default="")
OIDC_OP_USER_ENDPOINT = env("OIDC_OP_USER_ENDPOINT", default="")
OIDC_OP_JWKS_ENDPOINT = env("OIDC_OP_JWKS_ENDPOINT", default="")
OIDC_RP_SIGN_ALGO = env("OIDC_RP_SIGN_ALGO", default="RS256")
OIDC_RP_SCOPES = "openid email profile groups"
OIDC_STORE_ACCESS_TOKEN = False
OIDC_STORE_REFRESH_TOKEN = False
OIDC_USERNAME_ALGO = "accounts.oidc_backend.generate_username"
# --- SimpleJWT ---
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=60),
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
"ROTATE_REFRESH_TOKENS": True,
"ALGORITHM": "HS256",
"AUTH_HEADER_TYPES": ("Bearer",),
}
# --- guardian ---
ANONYMOUS_USER_NAME = None
# --- Security Headers ---
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = "DENY"
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
# --- Content Security Policy (django-csp 3.x) ---
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'")
CSP_IMG_SRC = ("'self'", "data:")
CSP_FONT_SRC = ("'self'",)
CSP_CONNECT_SRC = ("'self'",)
CSP_FRAME_SRC = ("'none'",)
CSP_OBJECT_SRC = ("'none'",)
CSP_BASE_URI = ("'none'",)
CSP_FORM_ACTION = ("'self'",)
CSP_INCLUDE_NONCE_IN = ["script-src"]
# --- DRF Throttling ---
REST_FRAMEWORK["DEFAULT_THROTTLE_CLASSES"] = [
"rest_framework.throttling.AnonRateThrottle",
"rest_framework.throttling.UserRateThrottle",
]
REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = {
"anon": "100/hour",
"user": "2000/hour",
"login": "10/minute",
"upload": "30/hour",
"abuse": "20/minute",
"audit": "60/minute",
}
# --- Audit Log Retention (days per event class) ---
AUDIT_RETENTION_DAYS = {
"auth": 365 * 2,
"progress": 365 * 3,
"assessment": 365 * 3,
"trainer": 365 * 5,
"certificate": 365 * 10,
"access": 365,
"_default": 365,
}
# --- File Upload Policy ---
MAX_UPLOAD_SIZE_BYTES = env.int("MAX_UPLOAD_SIZE_MB", default=100) * 1024 * 1024
ALLOWED_UPLOAD_EXTENSIONS = env.list(
"ALLOWED_UPLOAD_EXTENSIONS",
default=[
".pdf", ".jpg", ".jpeg", ".png", ".gif",
".mp4", ".webm", ".avi", ".mov",
".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
".csv", ".txt",
],
)
# --- Security Audit Logging ---
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"security": {
"format": "%(asctime)s [SECURITY] %(levelname)s %(message)s",
},
},
"handlers": {
"console": {"class": "logging.StreamHandler"},
"security_console": {
"class": "logging.StreamHandler",
"formatter": "security",
},
},
"loggers": {
"security": {
"handlers": ["security_console"],
"level": "WARNING",
"propagate": False,
},
},
"root": {"handlers": ["console"], "level": "INFO"},
}