From 31ee1ba03e4ea5353b8c3fb464d73aba94e5cdef Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Mon, 21 Oct 2024 19:25:50 +0200 Subject: [PATCH] feat: Add nix setup --- .credentials/ANNUAIRE | 5 + .credentials/LDAP | 12 ++ .credentials/SECRET_KEY | 1 + annuaire/settings/__init__.py | 0 annuaire/settings/common.py | 123 ------------------- annuaire/settings/dev.py | 62 ---------- annuaire/settings/prod.py | 67 ---------- annuaire/settings/secret_example.py | 23 ---- {annuaire => app}/__init__.py | 0 app/settings.py | 181 ++++++++++++++++++++++++++++ {annuaire => app}/urls.py | 0 {annuaire => app}/wsgi.py | 0 default.nix | 45 +++++++ npins/default.nix | 80 ++++++++++++ npins/sources.json | 22 ++++ shell.nix | 40 +----- 16 files changed, 347 insertions(+), 314 deletions(-) create mode 100644 .credentials/ANNUAIRE create mode 100644 .credentials/LDAP create mode 100644 .credentials/SECRET_KEY delete mode 100644 annuaire/settings/__init__.py delete mode 100644 annuaire/settings/common.py delete mode 100644 annuaire/settings/dev.py delete mode 100644 annuaire/settings/prod.py delete mode 100644 annuaire/settings/secret_example.py rename {annuaire => app}/__init__.py (100%) create mode 100644 app/settings.py rename {annuaire => app}/urls.py (100%) rename {annuaire => app}/wsgi.py (100%) create mode 100644 default.nix create mode 100644 npins/default.nix create mode 100644 npins/sources.json diff --git a/.credentials/ANNUAIRE b/.credentials/ANNUAIRE new file mode 100644 index 0000000..0bfb1e6 --- /dev/null +++ b/.credentials/ANNUAIRE @@ -0,0 +1,5 @@ +{ + "PROTOCOL": "http", + "URL": "annuaire.example.com", + "PORT": 80 +} diff --git a/.credentials/LDAP b/.credentials/LDAP new file mode 100644 index 0000000..1412c01 --- /dev/null +++ b/.credentials/LDAP @@ -0,0 +1,12 @@ +{ + "SPI": { + "PROTOCOL": "ldaps", + "URL": "ldap.example.com", + "PORT": 636 + }, + "CRI": { + "PROTOCOL": "ldap", + "URL": "ldap.example.com", + "PORT": 636 + } +} diff --git a/.credentials/SECRET_KEY b/.credentials/SECRET_KEY new file mode 100644 index 0000000..de873cc --- /dev/null +++ b/.credentials/SECRET_KEY @@ -0,0 +1 @@ +insecure-key diff --git a/annuaire/settings/__init__.py b/annuaire/settings/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/annuaire/settings/common.py b/annuaire/settings/common.py deleted file mode 100644 index 064f0c3..0000000 --- a/annuaire/settings/common.py +++ /dev/null @@ -1,123 +0,0 @@ -""" -Paramètres communs pour l'annuaire. -""" - -from pathlib import Path - -from django.urls import reverse_lazy - -# ############################################################################# -# Secrets -# ############################################################################# - -try: - from . import secret -except ImportError: - raise ImportError( - "Le fichier `secret.py` est manquant.\n" - "Pour un environnement de développement, copiez `secret_example.py`." - ) - - -def get_secret(name: str): - """Shortcut to get a value from the `secret.py` file.""" - - if hasattr(secret, name): - return getattr(secret, name) - else: - raise RuntimeError(f"Le secret `{name}` est manquant.") - - -SECRET_KEY = get_secret("SECRET_KEY") -ADMINS = get_secret("ADMINS") -SERVER_EMAIL = get_secret("SERVER_EMAIL") -EMAIL_HOST = get_secret("EMAIL_HOST") - -# ############################################################################# -# Valeurs par défaut de Django -# ############################################################################# - -DEBUG = False - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent -DEFAULT_AUTO_FIELD = "django.db.models.AutoField" - -# Application definition -INSTALLED_APPS = [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", - "authens", - "fiches", -] - -MIDDLEWARE = [ - "django.middleware.security.SecurityMiddleware", - "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", -] - -ROOT_URLCONF = "annuaire.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", - ], - }, - }, -] - -AUTHENTICATION_BACKENDS = ( - "django.contrib.auth.backends.ModelBackend", - "fiches.backends.BackendFiches", -) - -WSGI_APPLICATION = "annuaire.wsgi.application" - -# ############################################################################# -# Paramètres de langue -# ############################################################################# - -LANGUAGE_CODE = "fr-fr" - -LANGUAGES = [ - ("fr", "Français"), - ("en", "English"), -] - -LOCALE_PATHS = [BASE_DIR / "locale"] - -TIME_ZONE = "UTC" - -USE_I18N = True -USE_L10N = True - -USE_TZ = True - -# ############################################################################# -# Paramètres CAS et LDAP -# ############################################################################# - -LOGIN_URL = reverse_lazy("authens:login") -LOGOUT_REDIRECT_URL = reverse_lazy("home") -AUTHENS_USE_OLDCAS = False -AUTHENS_USE_PASSWORD = False - -LDAP = get_secret("LDAP") -ANNUAIRE = get_secret("ANNUAIRE") diff --git a/annuaire/settings/dev.py b/annuaire/settings/dev.py deleted file mode 100644 index c1bad77..0000000 --- a/annuaire/settings/dev.py +++ /dev/null @@ -1,62 +0,0 @@ -""" -Paramètres de développement pour l'annuaire. -""" - -from django.urls import reverse_lazy - -from .common import * # noqa -from .common import BASE_DIR, INSTALLED_APPS, MIDDLEWARE - -# --- -# Tweaks for debug/local development -# --- - -ALLOWED_HOSTS = [] - -DEBUG = True -EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" - -STATIC_URL = "/static/" -MEDIA_URL = "/media/" -MEDIA_ROOT = BASE_DIR / "media" - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", - } -} - -# Use the default cache backend for local development -CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}} - -# Pas besoin de sécurité en local -AUTH_PASSWORD_VALIDATORS = [] -PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] - -LOGIN_URL = reverse_lazy("authens:login") -AUTHENS_USE_PASSWORD = True - -# ############################################################################# -# Debug tool bar -# ############################################################################# - - -def show_toolbar(request): - """ - On active la debug-toolbar en mode développement local sauf : - - dans l'admin où ça ne sert pas à grand chose; - - si la variable d'environnement DJANGO_NO_DDT est à 1 → ça permet de la désactiver - sans modifier ce fichier en exécutant `export DJANGO_NO_DDT=1` dans le terminal - qui lance `./manage.py runserver`. - - Autre side effect de cette fonction : on ne fait pas la vérification de INTERNAL_IPS - que ferait la debug-toolbar par défaut, ce qui la fait fonctionner aussi à - l'intérieur de Vagrant (comportement non testé depuis un moment…) - """ - return DEBUG and not request.path.startswith("/admin/") - - -INSTALLED_APPS = INSTALLED_APPS + ["debug_toolbar"] -MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + MIDDLEWARE -DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": show_toolbar} diff --git a/annuaire/settings/prod.py b/annuaire/settings/prod.py deleted file mode 100644 index 49c1943..0000000 --- a/annuaire/settings/prod.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -Paramètres de production pour l'annuaire. -""" - -import os - -from .common import * # noqa -from .common import BASE_DIR, get_secret - -# ############################################################################# -# Prod-specific secrets -# ############################################################################# - -REDIS_PASSWD = get_secret("REDIS_PASSWD") -REDIS_DB = get_secret("REDIS_DB") -REDIS_HOST = get_secret("REDIS_HOST") -REDIS_PORT = get_secret("REDIS_PORT") - -DBNAME = get_secret("DBNAME") -DBUSER = get_secret("DBUSER") -DBPASSWD = get_secret("DBPASSWD") - -ALLOWED_HOSTS = ["annuaire.eleves.ens.fr", "www.annuaire.eleves.ens.fr"] - -STATIC_ROOT = BASE_DIR.parent / "public" / "annuaire" / "static" - -STATIC_URL = "/static/" -MEDIA_ROOT = BASE_DIR / "media" -MEDIA_URL = "/media/" - -# ############################################################################# -# Cache settings -# ############################################################################# - -CACHES = { - "default": { - "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": "redis://:{passwd}@{host}:{port}/{db}".format( - passwd=REDIS_PASSWD, host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB - ), - } -} - -# ############################################################################# -# Prod database settings -# ############################################################################# - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.postgresql_psycopg2", - "NAME": DBNAME, - "USER": DBUSER, - "PASSWORD": DBPASSWD, - "HOST": os.environ.get("DBHOST", "localhost"), - } -} - - -AUTH_PASSWORD_VALIDATORS = map( - lambda v: {"NAME": f"django.contrib.auth.password_validation.{v}"}, - [ - "UserAttributeSimilarityValidator", - "MinimumLengthValidator", - "CommonPasswordValidator", - "NumericPasswordValidator", - ], -) diff --git a/annuaire/settings/secret_example.py b/annuaire/settings/secret_example.py deleted file mode 100644 index e87c8e7..0000000 --- a/annuaire/settings/secret_example.py +++ /dev/null @@ -1,23 +0,0 @@ -SECRET_KEY = "$=kp$3e=xh)*4h8(_g#lprlmve_vs9_xv9hlgse%+uk9nhc==x" -ADMINS = None -SERVER_EMAIL = "root@localhost" -EMAIL_HOST = None - -LDAP = { - "SPI": { - "PROTOCOL": "ldaps", - "URL": "ldap.example.com", - "PORT": 636, - }, - "CRI": { - "PROTOCOL": "ldap", - "URL": "ldap.example.com", - "PORT": 636, - }, -} - -ANNUAIRE = { - "PROTOCOL": "http", - "URL": "annuaire.example.com", - "PORT": 80, -} diff --git a/annuaire/__init__.py b/app/__init__.py similarity index 100% rename from annuaire/__init__.py rename to app/__init__.py diff --git a/app/settings.py b/app/settings.py new file mode 100644 index 0000000..1b4174d --- /dev/null +++ b/app/settings.py @@ -0,0 +1,181 @@ +""" +Django settings for the annuaire project +""" + +import os +from pathlib import Path + +from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _ +from loadcredential import Credentials + +credentials = Credentials(env_prefix="ANNUAIRE_") + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# WARNING: keep the secret key used in production secret! +SECRET_KEY = credentials["SECRET_KEY"] + +# WARNING: don't run with debug turned on in production! +DEBUG = credentials.get_json("DEBUG", False) + +ALLOWED_HOSTS = credentials.get_json("ALLOWED_HOSTS", []) + +ADMINS = credentials.get_json("ADMINS", []) + + +### +# List the installed applications + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "authens", + "fiches", +] + + +### +# List the installed middlewares + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "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", +] + + +### +# The main url configuration + +ROOT_URLCONF = "app.urls" + + +### +# Template configuration: +# - Django Templating Language is used +# - Application directories can be used + + +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", + ], + }, + }, +] + + +### +# Database configuration +# -> https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" + +DATABASES = credentials.get_json( + "DATABASES", + { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } + }, +) + +CACHES = credentials.get_json( + "CACHES", + default={ + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + }, + }, +) + + +### +# WSGI application configuration + +WSGI_APPLICATION = "app.wsgi.application" + + +### +# Staticfiles configuration + +STATIC_ROOT = credentials["STATIC_ROOT"] +STATIC_URL = "/static/" + +MEDIA_ROOT = credentials.get("MEDIA_ROOT", BASE_DIR / "media") +MEDIA_URL = "/media/" + + +### +# Internationalization configuration +# -> https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = "fr-fr" +TIME_ZONE = "Europe/Paris" + +USE_I18N = True +USE_L10N = True +USE_TZ = True + +LANGUAGES = [ + ("fr", _("Français")), + ("en", _("Anglais")), +] + +LOCALE_PATHS = [BASE_DIR / "locale"] + + +### +# Authentication configuration + +AUTHENS_USE_OLDCAS = False +AUTHENS_USE_PASSWORD = False + +LOGIN_URL = reverse_lazy("authens:login") +LOGOUT_REDIRECT_URL = reverse_lazy("home") + +AUTHENTICATION_BACKENDS = ( + "django.contrib.auth.backends.ModelBackend", + "fiches.backends.BackendFiches", +) + +AUTH_PASSWORD_VALIDATORS = map( + lambda v: {"NAME": f"django.contrib.auth.password_validation.{v}"}, + [ + "UserAttributeSimilarityValidator", + "MinimumLengthValidator", + "CommonPasswordValidator", + "NumericPasswordValidator", + ], +) + +LDAP = credentials.get_json("LDAP") +ANNUAIRE = credentials.get_json("ANNUAIRE") + +# Development settings +if DEBUG: + EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + AUTH_PASSWORD_VALIDATORS = [] + PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] + + LOGIN_URL = reverse_lazy("authens:login") + AUTHENS_USE_PASSWORD = True diff --git a/annuaire/urls.py b/app/urls.py similarity index 100% rename from annuaire/urls.py rename to app/urls.py diff --git a/annuaire/wsgi.py b/app/wsgi.py similarity index 100% rename from annuaire/wsgi.py rename to app/wsgi.py diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..f8528e2 --- /dev/null +++ b/default.nix @@ -0,0 +1,45 @@ +{ + sources ? import ./npins, + pkgs ? import sources.nixpkgs { }, +}: + +let + nix-pkgs = import sources.nix-pkgs { inherit pkgs; }; + + python3 = pkgs.python3.override { + packageOverrides = _: _: { + inherit (nix-pkgs) authens loadcredential; + }; + }; +in + +{ + devShell = pkgs.mkShell { + name = "annuaire.dev"; + + packages = [ + (python3.withPackages (ps: [ + ps.django + ps.pillow + ps.loadcredential + ps.authens + ps.python-dateutil + ])) + ]; + + env = { + DJANGO_SETTINGS_MODULE = "app.settings"; + + CREDENTIALS_DIRECTORY = builtins.toString ./.credentials; + + ANNUAIRE_DEBUG = builtins.toJSON true; + ANNUAIRE_STATIC_ROOT = builtins.toString ./.static; + }; + + shellHook = '' + if [ ! -d .static ]; then + mkdir .static + fi + ''; + }; +} diff --git a/npins/default.nix b/npins/default.nix new file mode 100644 index 0000000..5e7d086 --- /dev/null +++ b/npins/default.nix @@ -0,0 +1,80 @@ +# Generated by npins. Do not modify; will be overwritten regularly +let + data = builtins.fromJSON (builtins.readFile ./sources.json); + version = data.version; + + mkSource = + spec: + assert spec ? type; + let + path = + if spec.type == "Git" then + mkGitSource spec + else if spec.type == "GitRelease" then + mkGitSource spec + else if spec.type == "PyPi" then + mkPyPiSource spec + else if spec.type == "Channel" then + mkChannelSource spec + else + builtins.throw "Unknown source type ${spec.type}"; + in + spec // { outPath = path; }; + + mkGitSource = + { + repository, + revision, + url ? null, + hash, + branch ? null, + ... + }: + assert repository ? type; + # At the moment, either it is a plain git repository (which has an url), or it is a GitHub/GitLab repository + # In the latter case, there we will always be an url to the tarball + if url != null then + (builtins.fetchTarball { + inherit url; + sha256 = hash; # FIXME: check nix version & use SRI hashes + }) + else + assert repository.type == "Git"; + let + urlToName = + url: rev: + let + matched = builtins.match "^.*/([^/]*)(\\.git)?$" repository.url; + + short = builtins.substring 0 7 rev; + + appendShort = if (builtins.match "[a-f0-9]*" rev) != null then "-${short}" else ""; + in + "${if matched == null then "source" else builtins.head matched}${appendShort}"; + name = urlToName repository.url revision; + in + builtins.fetchGit { + url = repository.url; + rev = revision; + inherit name; + # hash = hash; + }; + + mkPyPiSource = + { url, hash, ... }: + builtins.fetchurl { + inherit url; + sha256 = hash; + }; + + mkChannelSource = + { url, hash, ... }: + builtins.fetchTarball { + inherit url; + sha256 = hash; + }; +in +if version == 3 then + builtins.mapAttrs (_: mkSource) data.pins +else + throw "Unsupported format version ${toString version} in sources.json. Try running `npins upgrade`" diff --git a/npins/sources.json b/npins/sources.json new file mode 100644 index 0000000..dc73589 --- /dev/null +++ b/npins/sources.json @@ -0,0 +1,22 @@ +{ + "pins": { + "nix-pkgs": { + "type": "Git", + "repository": { + "type": "Git", + "url": "https://git.hubrecht.ovh/hubrecht/nix-pkgs" + }, + "branch": "main", + "revision": "3e731378f3984313ef902c5e5a49e002e6e2c27e", + "url": null, + "hash": "1vy2dj9fyy653w6idvi1r73s0nd2a332a1xkppddjip6rk0i030p" + }, + "nixpkgs": { + "type": "Channel", + "name": "nixpkgs-unstable", + "url": "https://releases.nixos.org/nixpkgs/nixpkgs-24.11pre691017.b69de56fac8c/nixexprs.tar.xz", + "hash": "0z32pj0lh5ng2a6cn0qfmka8cynnygckn5615mkaxq2aplkvgzx3" + } + }, + "version": 3 +} \ No newline at end of file diff --git a/shell.nix b/shell.nix index 477bf9d..75d0ca0 100644 --- a/shell.nix +++ b/shell.nix @@ -1,40 +1,2 @@ -{ pkgs ? import { }, ... }: +(import ./. { }).devShell -let - nix-pre-commit-hooks = import (builtins.fetchTarball "https://github.com/cachix/pre-commit-hooks.nix/tarball/master"); - pre-commit-check = nix-pre-commit-hooks.run { - src = ./.; - hooks = { - black.enable = true; - isort.enable = true; - flake8.enable = true; - }; - }; - - mkSetup = self: super: pkg: super.${pkg}.overridePythonAttrs (old: { buildInputs = (old.buildInputs or [ ]) ++ [ self.setuptools ]; }); - - poetryEnv = pkgs.poetry2nix.mkPoetryEnv { - projectDir = ./.; - python = pkgs.python39; - preferWheels = true; - - overrides = pkgs.poetry2nix.overrides.withDefaults (self: super: { - python-ldap = mkSetup self super "python-ldap"; - - authens = mkSetup self super "authens"; - }); - }; -in - -pkgs.mkShell { - buildInputs = [ - pkgs.poetry - poetryEnv - ]; - - shellHook = '' - ${pre-commit-check.shellHook} - - export DJANGO_SETTINGS_MODULE="annuaire.settings.dev" - ''; -} -- 2.47.0