diff --git a/CLAUDE.md b/CLAUDE.md index 240a8f2..6ade42c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -149,6 +149,8 @@ All services (except `backup-runner`) have `*_MEM_LIMIT`/`*_CPU_LIMIT` env vars, ## Optional Feature Flags +**`ZGLOS_CAPTCHA_ENABLED`** (`1`) + **`ALTCHA_HMAC_KEY`** (64-hex): ALTCHA proof-of-work captcha on the **public** (anonymous-only) publication-submission form. Both are written by the `_ensure_var` self-heal in `scripts/ensure-config-files.sh` — the single place; `init-configs.sh` calls that script, so fresh installs are covered too (do **not** add a second copy to the `.env` heredoc). **Order is load-bearing: key first, flag second** — a flag without a real key is worthless (Django falls back to a public sentinel → forgeable challenges). The flag is written **independently** of whether the key was just generated: installs that already got the key alone (since PR #19) would never light up if the two were coupled. Never rotate an existing key on a later `make up` — it invalidates challenges held by open forms. Operator opt-out is `ZGLOS_CAPTCHA_ENABLED=0`; `_ensure_var` never overwrites a non-empty value, so it survives. No Compose change needed: both reach Django via the wholesale `env_file`. Needs a BPP image ≥ `202607.1398`; older images ignore both. Operator doc: `docs/konfiguracja/architektura.md`. + **`DJANGO_BPP_ENABLE_HTML2DOCX_IMAGE`** (default `false`): when `true`, `make pull`/`make up` pulls `iplweb/html2docx:latest` as a fallback for HTML→DOCX export. Most installs use pandoc in the appserver image — enable only when pandoc fails. Deployment-side flag only, not propagated to Django. ## Backwards Compatibility and `.env` Migrations — CRITICAL diff --git a/docs/konfiguracja/architektura.md b/docs/konfiguracja/architektura.md index a4bf26c..d20d1dc 100644 --- a/docs/konfiguracja/architektura.md +++ b/docs/konfiguracja/architektura.md @@ -114,6 +114,54 @@ Zmienna jest ustawiana automatycznie: Możesz nadpisać wartość ręcznie w `.env` (np. inny punkt montowania) — self-heal jej nie ruszy. **Bez cudzysłowów** — `validate-env-quotes` odrzuca wartości w cudzysłowach. +## Captcha zgłoszeń publikacji — `ZGLOS_CAPTCHA_ENABLED` i `ALTCHA_HMAC_KEY` + +Publiczny formularz zgłaszania publikacji jest dostępny bez logowania, więc widzą go też +boty. Chroni go **ALTCHA** — captcha typu proof-of-work: przeglądarka liczy zadanie +obliczeniowe w tle, bez klikania w zdjęcia. Jest self-hosted (żadnych usług zewnętrznych, +żadnych danych osobowych wysyłanych na zewnątrz). + +Captcha dotyczy **wyłącznie niezalogowanych**. Zalogowany użytkownik nie zobaczy jej +nigdy, a obok widgetu jest podpowiedź, że zalogowanie pomija weryfikację. + +Dwie zmienne w `.env`: + +| Zmienna | Wartość | Znaczenie | +|---|---|---| +| `ALTCHA_HMAC_KEY` | 64 znaki hex (losowe) | Klucz podpisujący wyzwania ALTCHA | +| `ZGLOS_CAPTCHA_ENABLED` | `1` / `0` | Włącza captchę (`0` = wyłączona) | + +Obie ustawiają się automatycznie, bez ręcznego kroku — dopisuje je +`scripts/ensure-config-files.sh` przy każdym `make up`/`refresh` (a `make init-configs` +woła ten skrypt pod spodem, więc nowe instalacje dostają je tak samo). Na starym `.env` +wystarczy `git pull && make up`. + +Klucz jest generowany **raz** i potem nietykany — kolejne `make up` go nie rotują +(rotacja unieważniłaby wyzwania trzymane przez otwarte w przeglądarkach formularze). + +**Aby wyłączyć captchę**, ustaw w `.env`: + +``` +ZGLOS_CAPTCHA_ENABLED=0 +``` + +Wartość przeżyje kolejne `git pull && make up` — self-heal nie nadpisuje istniejących +wartości. Samo **usunięcie linii nie wystarczy**: zostanie dopisana z powrotem. + +!!! warning "Nie włączaj captchy bez losowego klucza" + `ZGLOS_CAPTCHA_ENABLED=1` przy braku (albo placeholderze) `ALTCHA_HMAC_KEY` daje + captchę **możliwą do podrobienia** — klucz podpisujący jest wtedy znany publicznie. + Automatyka pilnuje kolejności (klucz zawsze przed flagą). Django sygnalizuje zły stan + ostrzeżeniem `zglos_publikacje.W001` przy starcie. Jeśli dopisujesz zmienne ręcznie — + dopisz **obie**: + + ```bash + openssl rand -hex 32 # wynik wklej jako ALTCHA_HMAC_KEY + ``` + +Captcha wymaga obrazu BPP z ALTCHA (wydania od `202607.1398` wzwyż). Na starszym obrazie +zmienne są nieszkodliwe — Django ich po prostu nie czyta. + ## Pierwsze uruchomienie — dwa przebiegi `make` ```bash diff --git a/scripts/ensure-config-files.sh b/scripts/ensure-config-files.sh index 70f2c8b..07273a2 100755 --- a/scripts/ensure-config-files.sh +++ b/scripts/ensure-config-files.sh @@ -167,11 +167,24 @@ if [ -f "$_ENV" ]; then _ensure_secret NTFY_TOPIC "bpp-$(openssl rand -hex 16)" # Klucz HMAC do proof-of-work CAPTCHA ALTCHA na anonimowym formularzu # zgloszen publikacji (bpp PR #560). 64-hex, jak wymaga django-altcha. - # Sama captcha jest domyslnie WYLACZONA (ZGLOS_CAPTCHA_ENABLED) - klucz - # generujemy z wyprzedzeniem, zeby wlaczenie bylo jednym krokiem operatora, - # bez recznego generowania sekretu. Wszystkie serwisy Django czytaja .env - # przez env_file, wiec zaden dodatkowy wpis w compose nie jest potrzebny. + # Wszystkie serwisy Django czytaja .env przez env_file, wiec zaden dodatkowy + # wpis w compose nie jest potrzebny. _ensure_secret ALTCHA_HMAC_KEY "$(openssl rand -hex 32)" + # ...i od razu zapal captche. KOLEJNOSC JEST ISTOTNA: flage dopisujemy PO + # kluczu, wiec w momencie jej zapalenia realny klucz juz jest w .env. Captcha + # z nieustawionym kluczem byla by bezwartosciowa - Django bierze wtedy + # publiczny sentinel-placeholder, wiec wyzwanie da sie podrobic. + # + # Flaga jest DOPISYWANA NIEZALEZNIE od tego, czy klucz powstal wlasnie teraz: + # instalacje ktore dostaly sam klucz wczesniej (bpp-deploy generuje go od + # 2026-07-13, PR #19) maja go juz w .env, wiec sprzegniecie flagi z generacja + # klucza nigdy by ich nie zapalilo - a to wlasnie one czekaja na captche. + # + # Operator wylacza captche przez ZGLOS_CAPTCHA_ENABLED=0 - _ensure_var nie + # nadpisuje niepustych wartosci, wiec wybor przezyje kolejne `make up` + # (samo usuniecie linii nie wystarczy: wroci jako brakujaca). + _ensure_var ZGLOS_CAPTCHA_ENABLED "1" \ + " + wlaczono captche ALTCHA dla niezalogowanych na formularzu zgloszen (ZGLOS_CAPTCHA_ENABLED=1; wylaczysz ustawiajac 0)" # Media root: stala wartosc = punkt montowania wolumenu 'media' (/mediaroot) # we wszystkich kontenerach Django. Bez niej Django bierze swoj domyslny # MEDIA_ROOT (~/bpp-media = /root/bpp-media w kontenerze), POZA wolumenem - diff --git a/tests/test_makefile.sh b/tests/test_makefile.sh index cb63ecb..159a9aa 100755 --- a/tests/test_makefile.sh +++ b/tests/test_makefile.sh @@ -1034,6 +1034,127 @@ test_configure_resources_worker_consolidation() { rm -rf "$cfg" } +# ============================================================ +# TEST: init-configs — ALTCHA (klucz HMAC + flaga captchy) w swiezym .env +# ============================================================ +# Captcha zgloszen publikacji jest bezwartosciowa bez losowego klucza HMAC +# (znany klucz => wyzwanie da sie podrobic), wiec swiezy .env musi dostac oba. +test_init_configs_generates_altcha() { + yellow "=== Test: init-configs generuje ALTCHA_HMAC_KEY + ZGLOS_CAPTCHA_ENABLED ===" + + setup_temp + mkdir -p "$CONFIG_DIR" + + make -C "$REPO_COPY" init-configs BPP_CONFIGS_DIR="$CONFIG_DIR" >/dev/null 2>&1 + + assert_file_contains "ZGLOS_CAPTCHA_ENABLED=1 w .env" \ + "ZGLOS_CAPTCHA_ENABLED=1" "$CONFIG_DIR/.env" + + # `|| true`: pod `set -e` puste grep (=1) ubiloby caly przebieg testow + # zamiast zaraportowac FAIL ponizej. + local key + key=$(grep '^ALTCHA_HMAC_KEY=' "$CONFIG_DIR/.env" | cut -d= -f2 || true) + # 32 bajty losowe = 64 znaki hex (openssl rand -hex 32). + if printf '%s' "$key" | grep -qE '^[0-9a-f]{64}$'; then + pass "ALTCHA_HMAC_KEY to 64 znaki hex" + else + fail "ALTCHA_HMAC_KEY to 64 znaki hex (jest: '${key}')" + fi + + cleanup_temp +} + +# ============================================================ +# TEST: ensure-config-files — self-heal ALTCHA na starym .env +# ============================================================ +# `git pull && make up` na .env sprzed captchy musi zapalic ja bez recznego kroku +# (regula kompatybilnosci wstecznej z CLAUDE.md). Jednoczesnie: klucz nie moze sie +# regenerowac przy kazdym `make up` (rotacja = uniewaznienie wyzwan w locie), a +# swiadome ZGLOS_CAPTCHA_ENABLED=0 musi przezyc upgrade. +test_ensure_config_files_altcha_selfheal() { + yellow "=== Test: ensure-config-files dosypuje ALTCHA do starego .env ===" + + local cfg + cfg=$(mktemp -d) + + # Stary .env: bez ALTCHA_HMAC_KEY i bez ZGLOS_CAPTCHA_ENABLED. + printf 'BPP_CONFIGS_DIR=%s\nDJANGO_BPP_SECRET_KEY=stary-sekret\n' "$cfg" > "$cfg/.env" + + if ! BPP_CONFIGS_DIR="$cfg" bash "$REPO_DIR/scripts/ensure-config-files.sh" >/dev/null 2>&1; then + fail "ensure-config-files zwrocil blad (stary .env)" + rm -rf "$cfg"; return + fi + + assert_file_contains "self-heal: ZGLOS_CAPTCHA_ENABLED=1" \ + "ZGLOS_CAPTCHA_ENABLED=1" "$cfg/.env" + + local key + key=$(grep '^ALTCHA_HMAC_KEY=' "$cfg/.env" | cut -d= -f2 || true) + if printf '%s' "$key" | grep -qE '^[0-9a-f]{64}$'; then + pass "self-heal: ALTCHA_HMAC_KEY to 64 znaki hex" + else + fail "self-heal: ALTCHA_HMAC_KEY to 64 znaki hex (jest: '${key}')" + fi + + # Idempotencja: drugi przebieg NIE rotuje klucza. + if ! BPP_CONFIGS_DIR="$cfg" bash "$REPO_DIR/scripts/ensure-config-files.sh" >/dev/null 2>&1; then + fail "ensure-config-files zwrocil blad (drugi przebieg)" + rm -rf "$cfg"; return + fi + local key2 + key2=$(grep '^ALTCHA_HMAC_KEY=' "$cfg/.env" | cut -d= -f2 || true) + if [ "$key" = "$key2" ]; then + pass "ALTCHA_HMAC_KEY stabilny miedzy przebiegami" + else + fail "ALTCHA_HMAC_KEY stabilny miedzy przebiegami (zrotowal sie)" + fi + # Dokladnie jedna linia z kluczem (brak duplikatu klucza w .env). + local key_lines + key_lines=$(grep -c '^ALTCHA_HMAC_KEY=' "$cfg/.env" || true) + if [ "$key_lines" = "1" ]; then + pass "ALTCHA_HMAC_KEY wystepuje raz" + else + fail "ALTCHA_HMAC_KEY wystepuje raz (jest: $key_lines)" + fi + + rm -rf "$cfg" + + # Instalacja ktora dostala sam klucz (bpp-deploy generuje go od PR #19), ale + # nigdy flagi - to najliczniejszy przypadek w praktyce (m.in. staging). + # Flaga MUSI sie dopisac mimo ze klucz juz istnieje, a klucz zostac nietkniety. + cfg=$(mktemp -d) + local stary_klucz="aaaaaaaabbbbbbbbccccccccddddddddeeeeeeeeffffffff0000000011111111" + printf 'BPP_CONFIGS_DIR=%s\nALTCHA_HMAC_KEY=%s\n' "$cfg" "$stary_klucz" > "$cfg/.env" + + if ! BPP_CONFIGS_DIR="$cfg" bash "$REPO_DIR/scripts/ensure-config-files.sh" >/dev/null 2>&1; then + fail "ensure-config-files zwrocil blad (.env z samym kluczem)" + rm -rf "$cfg"; return + fi + + assert_file_contains "istniejacy klucz + brak flagi => flaga dopisana" \ + "ZGLOS_CAPTCHA_ENABLED=1" "$cfg/.env" + assert_file_contains "istniejacy klucz NIE zostal podmieniony" \ + "ALTCHA_HMAC_KEY=$stary_klucz" "$cfg/.env" + + rm -rf "$cfg" + + # Swiadome wylaczenie captchy przez operatora przezywa `make up`. + cfg=$(mktemp -d) + printf 'BPP_CONFIGS_DIR=%s\nZGLOS_CAPTCHA_ENABLED=0\n' "$cfg" > "$cfg/.env" + + if ! BPP_CONFIGS_DIR="$cfg" bash "$REPO_DIR/scripts/ensure-config-files.sh" >/dev/null 2>&1; then + fail "ensure-config-files zwrocil blad (.env z captcha OFF)" + rm -rf "$cfg"; return + fi + + assert_file_contains "ZGLOS_CAPTCHA_ENABLED=0 nienaruszone" \ + "ZGLOS_CAPTCHA_ENABLED=0" "$cfg/.env" + assert_file_not_contains "captcha OFF nie zostala wlaczona" \ + "ZGLOS_CAPTCHA_ENABLED=1" "$cfg/.env" + + rm -rf "$cfg" +} + # ============================================================ # Run # ============================================================ @@ -1063,6 +1184,8 @@ test_nginx_config_valid test_nginx_runtime test_configure_resources test_configure_resources_worker_consolidation +test_init_configs_generates_altcha +test_ensure_config_files_altcha_selfheal echo "" echo "========================================"