Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 48 additions & 0 deletions docs/konfiguracja/architektura.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 17 additions & 4 deletions scripts/ensure-config-files.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 -
Expand Down
123 changes: 123 additions & 0 deletions tests/test_makefile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ============================================================
Expand Down Expand Up @@ -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 "========================================"
Expand Down