diff --git a/.env-example b/.env-example new file mode 100644 index 0000000..71328b3 --- /dev/null +++ b/.env-example @@ -0,0 +1,23 @@ +# Django core +DEBUG=True +SECRET_KEY=dev-secret-key +DJANGO_SETTINGS_MODULE=config.settings + +# Superuser (optional) +DJANGO_SUPERUSER_USERNAME=admin +DJANGO_SUPERUSER_EMAIL=admin@example.com +DJANGO_SUPERUSER_PASSWORD=changeme + +# OIDC / SSO +SSO_ENABLED=False +OIDC_RP_CLIENT_ID=django-app +OIDC_RP_CLIENT_SECRET=changeme +OIDC_OP_DISCOVERY_ENDPOINT=https://auth.example.com/.well-known/openid-configuration + +# Database settings (Default: SQLite) +DB_ENGINE=sqlite +DB_NAME=db.sqlite3 +DB_USER= +DB_PASSWORD= +DB_HOST= +DB_PORT= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1d17dae..f395f46 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ .venv +.startserver.sh +.env +sb.sqlite3 \ No newline at end of file diff --git a/api/__pycache__/__init__.cpython-311.pyc b/api/__pycache__/__init__.cpython-311.pyc index 9a9a706..2fb33de 100644 Binary files a/api/__pycache__/__init__.cpython-311.pyc and b/api/__pycache__/__init__.cpython-311.pyc differ diff --git a/api/__pycache__/views.cpython-311.pyc b/api/__pycache__/views.cpython-311.pyc index a236f54..d70b87c 100644 Binary files a/api/__pycache__/views.cpython-311.pyc and b/api/__pycache__/views.cpython-311.pyc differ diff --git a/api/views.py b/api/views.py index 4665146..a5a49ba 100644 --- a/api/views.py +++ b/api/views.py @@ -1,8 +1,20 @@ from rest_framework.decorators import api_view, permission_classes -from rest_framework.permissions import AllowAny +from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response + @api_view(["GET"]) -@permission_classes([AllowAny]) # erstmal offen, später absichern +@permission_classes([AllowAny]) # This endpoint is deliberately open to everyone def ping(request): - return Response({"status": "ok"}) \ No newline at end of file + return Response({"status": "ok"}) + + +@api_view(["GET"]) +@permission_classes([IsAuthenticated]) # Requires either session (OIDC) or basic authentication +def secure_ping(request): + return Response({ + "status": "ok", + "user": request.user.username, # The authenticated username + # Indicates whether the request was authenticated via session (OIDC) or via basic auth + "auth_via": request.auth.__class__.__name__ if request.auth else "session/basic" + }) \ No newline at end of file diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc index 2579686..5106d16 100644 Binary files a/config/__pycache__/__init__.cpython-311.pyc and b/config/__pycache__/__init__.cpython-311.pyc differ diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 6a0e5f5..6d762af 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index c5ad2ae..0dd6577 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/__pycache__/wsgi.cpython-311.pyc b/config/__pycache__/wsgi.cpython-311.pyc index 01cbb7e..979f373 100644 Binary files a/config/__pycache__/wsgi.cpython-311.pyc and b/config/__pycache__/wsgi.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 1c18edc..ffd02cd 100644 --- a/config/settings.py +++ b/config/settings.py @@ -1,42 +1,47 @@ -import environ import os - from pathlib import Path +import environ -# Build paths inside the project like this: BASE_DIR / 'subdir'. +# --------------------------------------------------------------------------- +# Base project paths +# --------------------------------------------------------------------------- BASE_DIR = Path(__file__).resolve().parent.parent -# Load Env-Vars +# --------------------------------------------------------------------------- +# Environment configuration +# --------------------------------------------------------------------------- env = environ.Env() environ.Env.read_env(os.path.join(BASE_DIR, ".env")) -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! +# --------------------------------------------------------------------------- +# Security settings +# --------------------------------------------------------------------------- SECRET_KEY = env( "SECRET_KEY", - default="django-insecure-lbfv*h@=mjj#xq^!k@-5f2oiq@u6ms9t6=3&nr+!#itih%jh^l" + default="django-insecure-lbfv*h@=mjj#xq^!k@-5f2oiq@u6ms9t6=3&nr+!#itih%jh^l" # fallback only for development ) +DEBUG = env.bool("DEBUG", default=False) +ALLOWED_HOSTS = ["localhost", "127.0.0.1"] -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = env.bool("DEBUG", default=True) - -ALLOWED_HOSTS = ["localhost","127.0.0.1"] - - -# Application definition - +# --------------------------------------------------------------------------- +# Installed apps +# --------------------------------------------------------------------------- INSTALLED_APPS = [ + # Django built-in apps "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + + # Third-party apps "rest_framework", ] +# --------------------------------------------------------------------------- +# Middleware +# --------------------------------------------------------------------------- MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", @@ -47,12 +52,19 @@ MIDDLEWARE = [ "django.middleware.clickjacking.XFrameOptionsMiddleware", ] +# --------------------------------------------------------------------------- +# URL & WSGI configuration +# --------------------------------------------------------------------------- ROOT_URLCONF = "config.urls" +WSGI_APPLICATION = "config.wsgi.application" +# --------------------------------------------------------------------------- +# Templates +# --------------------------------------------------------------------------- TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], + "DIRS": [], # can be extended if custom templates are needed "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -65,89 +77,100 @@ TEMPLATES = [ }, ] -WSGI_APPLICATION = "config.wsgi.application" +# --------------------------------------------------------------------------- +# Database configuration +# --------------------------------------------------------------------------- +DB_ENGINE = env("DB_ENGINE", default="sqlite").lower() - -# Database -# https://docs.djangoproject.com/en/4.2/ref/settings/#databases - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", +if DB_ENGINE == "postgres": + DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": env("DB_NAME", default="postgres"), + "USER": env("DB_USER", default="postgres"), + "PASSWORD": env("DB_PASSWORD", default=""), + "HOST": env("DB_HOST", default="localhost"), + "PORT": env("DB_PORT", default="5432"), + } } -} +elif DB_ENGINE == "mysql": + DATABASES = { + "default": { + "ENGINE": "django.db.backends.mysql", + "NAME": env("DB_NAME", default="mysql"), + "USER": env("DB_USER", default="root"), + "PASSWORD": env("DB_PASSWORD", default=""), + "HOST": env("DB_HOST", default="localhost"), + "PORT": env("DB_PORT", default="3306"), + "OPTIONS": { + "charset": "utf8mb4", # recommended for full Unicode support + }, + } + } -# Password validation -# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators +else: # default: SQLite + DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", # fixed filename for simplicity + } + } +# --------------------------------------------------------------------------- +# Authentication & password validation +# --------------------------------------------------------------------------- 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", - }, + {"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/4.2/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/4.2/howto/static-files/ - +# --------------------------------------------------------------------------- +# Static files +# --------------------------------------------------------------------------- STATIC_URL = "static/" +# --------------------------------------------------------------------------- # Default primary key field type -# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field - +# --------------------------------------------------------------------------- DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +# --------------------------------------------------------------------------- +# Django REST framework configuration +# --------------------------------------------------------------------------- REST_FRAMEWORK = { "DEFAULT_PERMISSION_CLASSES": [ - "rest_framework.permissions.IsAuthenticated", + "rest_framework.permissions.IsAuthenticated", # all endpoints protected by default ], "DEFAULT_AUTHENTICATION_CLASSES": [ - "rest_framework.authentication.SessionAuthentication", - "rest_framework.authentication.BasicAuthentication", + "rest_framework.authentication.SessionAuthentication", # required for OIDC/session login + "rest_framework.authentication.BasicAuthentication", # allows Basic Auth for API clients ], } -# OIDC Vars - +# --------------------------------------------------------------------------- +# OpenID Connect (SSO) configuration +# --------------------------------------------------------------------------- SSO_ENABLED = env.bool("SSO_ENABLED", default=False) if SSO_ENABLED: - INSTALLED_APPS += [ - "mozilla_django_oidc", - ] - - MIDDLEWARE += [ - "mozilla_django_oidc.middleware.SessionRefresh", - ] + INSTALLED_APPS += ["mozilla_django_oidc"] + MIDDLEWARE += ["mozilla_django_oidc.middleware.SessionRefresh"] LOGIN_URL = "/oidc/authenticate/" LOGIN_REDIRECT_URL = "/" LOGOUT_REDIRECT_URL = "/" - OIDC_RP_CLIENT_ID = env("OIDC_RP_CLIENT_ID", default="django-app") OIDC_RP_CLIENT_SECRET = env("OIDC_RP_CLIENT_SECRET", default="changeme") OIDC_OP_DISCOVERY_ENDPOINT = env( @@ -157,4 +180,4 @@ if SSO_ENABLED: OIDC_RP_SIGN_ALGO = "RS256" OIDC_STORE_ID_TOKEN = True - OIDC_STORE_ACCESS_TOKEN = True \ No newline at end of file + OIDC_STORE_ACCESS_TOKEN = True diff --git a/config/urls.py b/config/urls.py index 52f3b99..4afde4b 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,14 +1,17 @@ from django.conf import settings from django.contrib import admin from django.urls import path, include -from api.views import ping +from api.views import ping, secure_ping + urlpatterns = [ path("admin/", admin.site.urls), - path("api/ping/", ping), + path("api/ping/", ping), # Public healthcheck endpoint + path("api/secure-ping/", secure_ping) # Protected API endpoint ] +# Add OIDC routes only if Single Sign-On is enabled if settings.SSO_ENABLED: urlpatterns += [ path("oidc/", include("mozilla_django_oidc.urls")), - ] \ No newline at end of file + ] diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000..6c26050 Binary files /dev/null and b/db.sqlite3 differ diff --git a/docker-compose-sqlite.yml b/docker-compose-sqlite.yml new file mode 100644 index 0000000..715b72b --- /dev/null +++ b/docker-compose-sqlite.yml @@ -0,0 +1,9 @@ +--- +services: + risk-management: + build: . + container_name: django_web + ports: + - "8000:8000" + volumes: + - ./data/app/:/app \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh index faccadb..adc88b1 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,6 +1,8 @@ #!/bin/sh set -e +export DJANGO_SETTINGS_MODULE=config.settings + echo "Running migrations..." python manage.py migrate --noinput diff --git a/startserver.sh b/startserver.sh new file mode 100755 index 0000000..84f24c3 --- /dev/null +++ b/startserver.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# ----------------------------------------------------------------------------- +# Startup script for the Django project +# ----------------------------------------------------------------------------- +# This script ensures that the development environment is ready to run. +# +# Features: +# - Runs database migrations +# - Creates a superuser if it does not already exist +# - Starts the Django development server +# +# Note: +# This script is for local development only. +# In production you should use a proper WSGI/ASGI server (e.g., Gunicorn, Daphne). +# ----------------------------------------------------------------------------- + +set -e # Exit immediately if a command exits with a non-zero status + +# Load environment variables from .env if available +if [ -f ".env" ]; then + export $(grep -v '^#' .env | xargs) +fi + +# Ensure we are in the project root directory +cd "$(dirname "$0")" + +echo ">>> Running migrations..." +python manage.py migrate --noinput + +echo ">>> Checking if superuser exists..." +python manage.py shell <>> Starting Django development server at http://127.0.0.1:8000 ..." +python manage.py runserver 0.0.0.0:8000 \ No newline at end of file