diff --git a/containers/authentik/authentik-django.nix b/containers/authentik/authentik-django.nix new file mode 100644 index 0000000..0844769 --- /dev/null +++ b/containers/authentik/authentik-django.nix @@ -0,0 +1,143 @@ +# Authentik Python/Django backend +# +# Assembles the final package from: +# 1. python-deps FOD (venv with stripped store references) +# 2. opencontainers git dependency (fetched via Nix) +# 3. Workspace packages (ak-guardian, django-channels-postgres, etc.) +# 4. Authentik application source +# 5. Lifecycle scripts, blueprints, manage.py +# +# autoPatchelfHook restores RPATHs that were stripped in the FOD. +# +# Output: +# $out/bin/python3.14 venv python (symlink to nix python314) +# $out/lib/python3.14/site-packages/ all Python packages +# $out/lifecycle/ lifecycle scripts (symlink) +# $out/blueprints/ YAML blueprints +# $out/manage.py Django management script +{ pkgs ? import { }, sources ? import ./sources.nix { inherit pkgs; } }: + +let + python-deps = import ./python-deps.nix { inherit pkgs sources; }; + + # opencontainers is a git dependency not on PyPI — fetch separately + opencontainers-src = pkgs.fetchFromGitHub { + owner = "vsoch"; + repo = "oci-python"; + rev = "ceb4fcc090851717a3069d78e85ceb1e86c2740c"; + hash = pkgs.lib.fakeHash; + }; + + sp = "$out/lib/python3.14/site-packages"; +in + +pkgs.stdenv.mkDerivation { + pname = "authentik-django"; + version = sources.version; + inherit (sources) meta; + + src = sources.src; + + nativeBuildInputs = with pkgs; [ + autoPatchelfHook # restores RPATHs stripped in the FOD + ]; + + # Libraries that autoPatchelfHook resolves NEEDED entries against + buildInputs = with pkgs; [ + python314 + stdenv.cc.cc.lib # libstdc++, libgcc_s + libxml2 + libxslt + xmlsec + openssl + libpq + krb5.lib + libtool.lib + libffi + zlib + ]; + + dontBuild = true; + + installPhase = '' + runHook preInstall + + # --- Copy venv from FOD --- + cp -r ${python-deps} $out + chmod -R +w $out + + # Restore python path in pyvenv.cfg (was replaced with @python@ in FOD) + sed -i "s|@python@|${pkgs.python314}|g" $out/pyvenv.cfg + + # Recreate bin/ (was removed in FOD to strip python store refs) + mkdir -p $out/bin + ln -s ${pkgs.python314}/bin/python3.14 $out/bin/python3.14 + ln -s python3.14 $out/bin/python3 + ln -s python3.14 $out/bin/python + + # Recreate entry point scripts that were in the venv's bin/ + # (gunicorn, etc. — use python from this venv) + for ep in gunicorn uvicorn dramatiq dumb-init; do + if [ -e ${sp}/$ep ] || $out/bin/python3.14 -c "import $ep" 2>/dev/null; then + cat > $out/bin/$ep << SCRIPT + #!$out/bin/python3.14 + import sys + from importlib.metadata import entry_points + eps = entry_points(group='console_scripts', name='$ep') + if eps: + sys.exit(eps[0].load()()) + SCRIPT + chmod +x $out/bin/$ep + fi + done 2>/dev/null || true + + # --- opencontainers (git dependency, pure Python) --- + cp -r ${opencontainers-src}/opencontainers ${sp}/opencontainers + + # --- Workspace packages (pure Python — direct copy) --- + # ak-guardian: hatch config maps to "guardian" package + cp -r packages/ak-guardian/guardian ${sp}/guardian + cp -r packages/django-channels-postgres/django_channels_postgres ${sp}/ + cp -r packages/django-dramatiq-postgres/django_dramatiq_postgres ${sp}/ + cp -r packages/django-postgres-cache/django_postgres_cache ${sp}/ + + # --- Authentik application + lifecycle --- + cp -r authentik ${sp}/authentik + cp -r lifecycle ${sp}/lifecycle + chmod +x ${sp}/lifecycle/ak + + # --- Patches for Nix store paths --- + + # BASE_DIR: point to $out instead of computing from settings.py's location + substituteInPlace ${sp}/authentik/root/settings.py \ + --replace-fail \ + 'BASE_DIR = Path(__file__).absolute().parent.parent.parent' \ + "BASE_DIR = Path(\"$out\")" + + # blueprints_dir: point to $out/blueprints + substituteInPlace ${sp}/authentik/lib/default.yml \ + --replace-fail 'blueprints_dir: /blueprints' \ + "blueprints_dir: $out/blueprints" + + # Web asset paths: placeholder @webui@ for Go server card to resolve + substituteInPlace ${sp}/authentik/stages/email/utils.py \ + --replace-fail 'Path("web/icons/icon_left_brand.png")' \ + 'Path("@webui@/icons/icon_left_brand.png")' \ + --replace-fail 'Path("web/dist/assets/icons/icon_left_brand.png")' \ + 'Path("@webui@/dist/assets/icons/icon_left_brand.png")' + + # Lifecycle bash script: use Nix store bash (no /usr/bin/env in containers) + substituteInPlace ${sp}/lifecycle/ak \ + --replace-fail '#!/usr/bin/env -S bash' '#!${pkgs.bash}/bin/bash' + + # --- Top-level structure --- + ln -s ${sp}/lifecycle $out/lifecycle + cp -r blueprints $out/blueprints + cp manage.py $out/manage.py + + runHook postInstall + ''; + + # autoPatchelfHook runs in fixupPhase — don't disable it + dontPatchShebangs = true; +} diff --git a/containers/authentik/python-deps.nix b/containers/authentik/python-deps.nix new file mode 100644 index 0000000..3530265 --- /dev/null +++ b/containers/authentik/python-deps.nix @@ -0,0 +1,133 @@ +# Fixed-output derivation (FOD): download and install all external Python +# dependencies into a venv using uv sync. +# +# FODs get network access because the output hash is declared upfront. +# However, FODs must not reference other Nix store paths in their output. +# Compiled .so files (from sdist builds) contain RPATHs to system libraries +# (libxml2, krb5, etc.) which are Nix store paths. We strip these references +# here; authentik-django.nix restores them via autoPatchelfHook. +# +# The venv's bin/ and pyvenv.cfg also reference the python store path, so we +# replace them with placeholders that the main derivation restores. +# +# When uv.lock changes, reset outputHash to pkgs.lib.fakeHash, build to +# get the correct hash from the error message, then update. +{ pkgs ? import { }, sources ? import ./sources.nix { inherit pkgs; } }: + +let + # All store paths that may end up referenced in the venv output. + # remove-references-to will replace each hash with 'eeee...' bytes. + refTargets = with pkgs; [ + python314 + stdenv.cc.cc.lib + libxml2.out + libxml2.dev + libxslt.out + libxslt.dev + xmlsec.out + openssl.out + openssl.dev + libpq.out + libpq.dev + krb5.out + krb5.dev + krb5.lib + libtool.out + libtool.lib + libffi.out + libffi.dev + zlib.out + zlib.dev + readline.out + ncurses.out + glibc.out + ]; + + removeRefsArgs = builtins.concatStringsSep " " + (map (t: "-t ${t}") refTargets); +in + +pkgs.stdenv.mkDerivation { + pname = "authentik-python-deps"; + version = sources.version; + + src = sources.src; + + nativeBuildInputs = with pkgs; [ + python314 + uv + git # opencontainers is a git dependency in uv.lock + cacert # HTTPS verification for PyPI + GitHub + pkg-config + removeReferencesTo + # Build tools on PATH for sdist compilation + postgresql.pg_config # pg_config for psycopg-c + krb5 # krb5-config for gssapi + ]; + + # System libraries for packages that must build from sdist: + # lxml, xmlsec — pyproject.toml [tool.uv] no-binary-package + # psycopg-c — sdist only on PyPI + # gssapi — no Linux wheels on PyPI + buildInputs = with pkgs; [ + libxml2 + libxslt + xmlsec + openssl + libpq # psycopg-c links against libpq + libtool # libltdl for xmlsec dynamic crypto backend loading + libffi + zlib + ]; + + buildPhase = '' + runHook preBuild + + export HOME=$TMPDIR + export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt + export GIT_SSL_CAINFO=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt + export UV_PYTHON=${pkgs.python314}/bin/python3.14 + export UV_LINK_MODE=copy + + # gssapi's pre-generated C code uses S4U functions declared in gssapi_ext.h + # but doesn't include it — force-include via compiler flag + export NIX_CFLAGS_COMPILE="''${NIX_CFLAGS_COMPILE:-} -include gssapi/gssapi_ext.h" + + uv sync \ + --frozen \ + --no-install-project \ + --no-install-workspace \ + --no-dev + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + mv .venv $out + + # --- Strip Nix store references (FODs must be self-contained) --- + # autoPatchelfHook in authentik-django.nix restores correct RPATHs. + + # Replace python store path in pyvenv.cfg with placeholder + sed -i "s|${pkgs.python314}|@python@|g" $out/pyvenv.cfg + + # Remove bin/ entirely — main derivation recreates it + rm -rf $out/bin + + # Strip store path references from shared objects + find $out -type f \( -name '*.so' -o -name '*.so.*' \) \ + -exec remove-references-to ${removeRefsArgs} {} + 2>/dev/null || true + + # Strip store refs from .pyc files (contain embedded paths) + find $out -type f -name '*.pyc' -delete + + runHook postInstall + ''; + + outputHashMode = "recursive"; + outputHashAlgo = "sha256"; + outputHash = pkgs.lib.fakeHash; + + dontFixup = true; +}