v0.7.40-beta - beheerdersaccount en bind meteen klaarzetten
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m14s
Deploy server-up (prod) / deploy (push) Successful in 1m44s
Release / release (push) Successful in 5s

Het installatiescript kan nu allebei zelf regelen, zodat je na afloop niet
meer naar de webinterface hoeft om een account te claimen en niet meer in
.env hoeft te duiken om erbij te kunnen.

Beheerdersaccount:
- Op een terminal wordt gevraagd of je er meteen een wil, met naam en
  wachtwoord (twee keer, echo uit).
- Automatisch via --admin NAAM plus --admin-password-file of
  SU_ADMIN_PASSWORD; zonder bron en zonder terminal maakt het script er
  zelf een van 24 tekens en toont die.
- --admin-password weigert bewust: argumenten zijn zichtbaar in 'ps'. Het
  wachtwoord gaat via stdin naar curl, niet als argument.
- Nieuw --create-admin voor een installatie die al draait.

Bereikbaarheid:
- Keuzemenu voor BIND (deze server / hele netwerk / eigen adres), met het
  gedetecteerde serveradres erbij.
- Bij opnieuw installeren is de bestaande instelling het uitgangspunt, zodat
  enter je server niet ongemerkt terugzet op loopback.

Opgeloste fouten:
- Een bestaande .env bleef altijd ongemoeid, ook met --bind: de installatie
  leek te lukken terwijl de server op het oude adres bleef luisteren.
- --update rekende met de standaardpoort in plaats van met wat er in .env
  staat, waardoor de wachtlus en het slotadres niet klopten.
- --dry-run riep 'sudo docker' aan en vroeg dus om een wachtwoord terwijl
  het net beloofd had niets te doen; zonder terminal liep het daarop vast.
- Ongeldig bind-adres en poort buiten 1-65535 worden nu meteen geweigerd.

Getest met een pseudo-terminal voor de vragen en tegen een echt draaiende
server voor het aanmaken van het account, inclusief een wachtwoord vol
aanhalingstekens en backslashes, een geweigerd tweede account en een server
die niet reageert. De e2e-tests slaan zichzelf over waar curl ontbreekt,
zoals in het CI-image. 992 tests groen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb
This commit is contained in:
Ramon 2026-07-27 21:13:06 +02:00
parent d7cbb4f440
commit 8e53b90615
5 changed files with 865 additions and 26 deletions

View file

@ -1,3 +1,52 @@
# v0.7.40-beta — Account en bereikbaarheid meteen goed
Na het installeren was je nog niet klaar: je moest zelf naar de webinterface om
een beheerdersaccount te claimen, en stond `BIND` op `127.0.0.1`, dan kón je daar
niet eens bij zonder eerst `.env` aan te passen. Het script regelt nu allebei.
**Beheerdersaccount.** Op een terminal vraagt het script of je er meteen één wil,
en zo ja om een naam en wachtwoord (twee keer, met de echo uit). Automatisch gaat
het met `--admin NAAM` plus `--admin-password-file PAD` of `SU_ADMIN_PASSWORD`.
Is er geen wachtwoordbron én geen terminal, dan maakt het script er zelf een van
24 tekens en zet die op het scherm.
Het wachtwoord kan bewust *niet* als optie mee: `--admin-password` weigert met
uitleg. Opdrachtregelargumenten zijn voor elke gebruiker op de server zichtbaar
met `ps` en blijven in je shell-geschiedenis staan. Onderweg naar de server gaat
het via stdin naar `curl`, niet als argument.
Nieuw is ook **`--create-admin`**: alleen het account aanmaken, bij een
installatie die al draait. Voor als het de eerste keer is misgegaan of je het
hebt overgeslagen. De poort komt daarbij uit `.env`.
**Bereikbaarheid.** Het script vraagt nu waarop de interface moet luisteren:
alleen deze server, het hele netwerk (met het gedetecteerde adres erbij), of een
adres dat je zelf opgeeft. Installeer je opnieuw over een bestaande map, dan is
je huidige instelling het uitgangspunt en houdt enter die vast.
Daarbij zat een fout die je zelf tegenkwam: een bestaande `.env` bleef *altijd*
ongemoeid, ook als je `--bind` meegaf. De installatie leek dan te lukken terwijl
de server op het oude adres bleef luisteren. Nu wordt doorgevoerd wat je
expliciet meegeeft, en blijft de rest staan. Andersom rekent het script bij
`--update` voortaan met wat er écht in `.env` staat: draaide je op poort 8080,
dan wachtte het daarvoor op poort 5000 en meldde het het verkeerde adres.
Verder: `--dry-run` riep `sudo docker` aan en vroeg dus om een wachtwoord,
terwijl het net beloofd had niets te doen — zonder terminal liep het daar vast.
Een proefdraai spreekt de Docker-daemon nu niet meer aan. En een ongeldig
bind-adres of een poort buiten 165535 wordt meteen geweigerd in plaats van als
een onbegrijpelijke fout uit `docker compose` terug te komen.
De vragen worden allemaal vooraf gesteld, zodat je kunt weglopen terwijl het
image gebouwd wordt.
Getest met een echte pseudo-terminal voor de vragen, en tegen een echt
draaiende server voor het aanmaken van het account — inclusief een wachtwoord
vol aanhalingstekens en backslashes, een tweede account dat geweigerd hoort te
worden, en een server die niet reageert.
---
# v0.7.30 — Installeren met één regel # v0.7.30 — Installeren met één regel
```bash ```bash

View file

@ -37,7 +37,9 @@ curl -fsSL https://git.ramonbesselink.nl/bes-r/server-up/raw/branch/main/install
``` ```
Dat controleert je systeem, installeert Docker als het ontbreekt, zet Server Up Dat controleert je systeem, installeert Docker als het ontbreekt, zet Server Up
in `/opt/server-up` en start het op `http://localhost:5000`. in `/opt/server-up` en start het. Onderweg vraagt het waarop de interface moet
luisteren en of het meteen een beheerdersaccount voor je aanmaakt — die twee
vragen komen vooraf, zodat je kunt weglopen terwijl het image gebouwd wordt.
Liever eerst lezen wat je uitvoert — verstandig, want het script gebruikt Liever eerst lezen wat je uitvoert — verstandig, want het script gebruikt
root-rechten: root-rechten:
@ -58,12 +60,25 @@ cp .env.example .env
docker compose up -d --build docker compose up -d --build
``` ```
Alle opties staan in [docs/installeren.md](docs/installeren.md): Alle opties staan in [docs/installeren.md](docs/installeren.md): `--dir`,
`--dir`, `--port`, `--bind`, `--branch`, `--token`, `--update`, `--uninstall`. `--port`, `--bind`, `--branch`, `--token`, `--admin`, `--create-admin`,
`--update`, `--uninstall`.
Open daarna de interface en **maak meteen een beheerdersaccount aan.** ### Het beheerdersaccount
Zolang dat niet gebeurd is, kan iedereen die de pagina bereikt het account
claimen — de container waarschuwt daar bij het opstarten ook voor. Zolang er nog geen account is, kan iedereen die de pagina bereikt het claimen —
de container waarschuwt daar bij het opstarten ook voor. Laat het script het
daarom meteen aanmaken:
```bash
sh install.sh --admin ramon # vraagt om een wachtwoord
SU_ADMIN_PASSWORD='...' sh install.sh --admin ramon --yes # zonder vragen
```
Zonder wachtwoordbron en zonder terminal maakt het script er zelf een en toont
die. Overgeslagen of misgegaan? `sh /opt/server-up/install.sh --create-admin
--admin ramon` doet het achteraf. Een wachtwoord als opdrachtregeloptie wordt
geweigerd: dat is zichtbaar in `ps`.
### Instellingen ### Instellingen
@ -76,7 +91,10 @@ PORT=5000
``` ```
De poort bindt standaard op `127.0.0.1`. Zet hem pas open als er een reverse De poort bindt standaard op `127.0.0.1`. Zet hem pas open als er een reverse
proxy met TLS voor staat. proxy met TLS voor staat. Achteraf wijzigen kan met
`sh /opt/server-up/install.sh --update --bind 0.0.0.0`; pas het anders aan in
`.env` en **niet** in `docker-compose.yml`, want dat bestand komt uit de repo en
wordt bij elke update overschreven.
## Documentatie ## Documentatie

View file

@ -50,6 +50,10 @@ schrijven én `docker` aanroepen, dan blijft alles onder je eigen account.
--bind ADRES Waarop de poort luistert (standaard: 127.0.0.1) --bind ADRES Waarop de poort luistert (standaard: 127.0.0.1)
--branch NAAM Branch om te installeren (standaard: main) --branch NAAM Branch om te installeren (standaard: main)
--token TOKEN Toegangstoken, als je repo niet openbaar is --token TOKEN Toegangstoken, als je repo niet openbaar is
--admin NAAM Maak meteen een beheerdersaccount met deze naam
--admin-password-file PAD
Lees het wachtwoord uit een bestand
--create-admin Alleen het account aanmaken, bij een draaiende installatie
--update Bijwerken naar de nieuwste versie --update Bijwerken naar de nieuwste versie
--uninstall Stoppen en verwijderen (gegevens blijven staan) --uninstall Stoppen en verwijderen (gegevens blijven staan)
--yes Niets vragen --yes Niets vragen
@ -59,12 +63,15 @@ schrijven én `docker` aanroepen, dan blijft alles onder je eigen account.
### Voorbeelden ### Voorbeelden
```bash ```bash
# Standaard, alleen bereikbaar vanaf de server zelf # Standaard: vraagt waarop het moet luisteren en of je een account wil
sh install.sh sh install.sh
# Bereikbaar op je netwerk, andere poort # Bereikbaar op je netwerk, andere poort
sh install.sh --bind 0.0.0.0 --port 8080 sh install.sh --bind 0.0.0.0 --port 8080
# Volledig automatisch, met account
SU_ADMIN_PASSWORD='een-lang-wachtwoord' sh install.sh --admin ramon --yes
# Ergens anders neerzetten # Ergens anders neerzetten
sh install.sh --dir /srv/server-up sh install.sh --dir /srv/server-up
@ -77,21 +84,65 @@ sh install.sh --token jouw-forgejo-token
--- ---
## Meteen na het installeren ## Het beheerdersaccount
**Maak een beheerdersaccount aan.** Server Up beheert de Docker-daemon, en wie Server Up beheert de Docker-daemon, en wie containers kan starten kan
containers kan starten kan willekeurige mappen van de host mounten. Zolang er willekeurige mappen van de host mounten. Zolang er nog geen account is, kan
nog geen account is, kan iedereen die de pagina bereikt het claimen. iedereen die de pagina bereikt het claimen — dus hoe korter dat venster, hoe
beter. Het script kan het account daarom meteen aanmaken.
De poort staat standaard op `127.0.0.1`, dus alleen bereikbaar vanaf de server Op een terminal vraagt het script erom. Automatisch kan het ook:
zelf. Wil je er van buitenaf bij, zet dan eerst een reverse proxy met TLS ervoor
en pas daarna `BIND` aan:
```bash ```bash
sed -i 's/^BIND=.*/BIND=0.0.0.0/' /opt/server-up/.env # Wachtwoord uit een bestand
docker compose -f /opt/server-up/docker-compose.yml up -d sh install.sh --admin ramon --admin-password-file /root/su-ww --yes
# Of uit een omgevingsvariabele
SU_ADMIN_PASSWORD='een-lang-wachtwoord' sh install.sh --admin ramon --yes
``` ```
Geef je geen wachtwoord en is er geen terminal, dan maakt het script er zelf een
van 24 tekens en zet die op het scherm. Schrijf die meteen over — hij staat
nergens anders.
> Het wachtwoord kan **niet** als optie mee. Opdrachtregelargumenten zijn voor
> elke gebruiker op de server zichtbaar met `ps` en blijven in je
> shell-geschiedenis staan. `--admin-password` weigert daarom met een verwijzing
> naar de twee manieren hierboven.
Ging het aanmaken mis, of heb je het overgeslagen? Dan kan het achteraf, zolang
er nog geen account bestaat:
```bash
sh /opt/server-up/install.sh --create-admin --admin ramon
```
Eist minstens 10 tekens, net als de webinterface.
---
## Bereikbaarheid
Het script vraagt waarop de webinterface moet luisteren:
| Keuze | `BIND` | Wanneer |
|---|---|---|
| Alleen deze server | `127.0.0.1` | Standaard en veiligst. Erbij via een SSH-tunnel of een reverse proxy op dezelfde machine. |
| Het hele netwerk | `0.0.0.0` | Direct bereikbaar op het adres van je server. Zet er een reverse proxy met TLS voor. |
| Een specifiek adres | zelf opgeven | Bijvoorbeeld alleen je beheernetwerk. |
Achteraf wijzigen kan met dezelfde optie; die wordt ook doorgevoerd in een
`.env` die er al staat:
```bash
sh /opt/server-up/install.sh --update --bind 0.0.0.0
```
Wat je *niet* expliciet meegeeft blijft staan zoals het was, zodat een `--update`
je instellingen niet terugzet. Pas `BIND` daarom aan in `.env` of via deze optie
**niet** in `docker-compose.yml`, want dat bestand komt uit de repo en wordt
bij elke update overschreven.
Zie [beveiliging.md](beveiliging.md) voor de reverse proxy, rollen en SSO. Zie [beveiliging.md](beveiliging.md) voor de reverse proxy, rollen en SSO.
--- ---

View file

@ -13,7 +13,7 @@
set -eu set -eu
SCRIPT_VERSIE="1.0" SCRIPT_VERSIE="1.1"
GIT_HOST="git.ramonbesselink.nl" GIT_HOST="git.ramonbesselink.nl"
GIT_PAD="bes-r/server-up" GIT_PAD="bes-r/server-up"
BRANCH="main" BRANCH="main"
@ -24,6 +24,13 @@ TOKEN=""
JA=0 JA=0
DROOG=0 DROOG=0
ACTIE="install" ACTIE="install"
BEHEERDER=""
BEHEERDER_WW=""
BEHEERDER_WW_BESTAND=""
# Expliciet meegegeven waarden overschrijven wél een bestaande .env; de rest
# blijft staan, zodat een --update je instellingen niet terugzet.
BIND_EXPLICIET=0
POORT_EXPLICIET=0
# ── Uitvoer ────────────────────────────────────────────────────────────────── # ── Uitvoer ──────────────────────────────────────────────────────────────────
# Kleuren alleen als we naar een terminal schrijven; in een pipe of logbestand # Kleuren alleen als we naar een terminal schrijven; in een pipe of logbestand
@ -79,15 +86,28 @@ Gebruik: install.sh [opties]
maar zet er dan wel een reverse proxy met TLS voor. maar zet er dan wel een reverse proxy met TLS voor.
--branch NAAM Branch om te installeren (standaard: ${BRANCH}) --branch NAAM Branch om te installeren (standaard: ${BRANCH})
--token TOKEN Toegangstoken, als je git-repo niet openbaar is --token TOKEN Toegangstoken, als je git-repo niet openbaar is
--admin NAAM Maak meteen een beheerdersaccount met deze naam
--admin-password-file PAD
Lees het wachtwoord uit een bestand. Kan ook via de
omgevingsvariabele SU_ADMIN_PASSWORD. Geef je geen van
beide, dan wordt ernaar gevraagd; kan dat niet (geen
terminal), dan wordt er één aangemaakt en getoond.
--create-admin Alleen het beheerdersaccount aanmaken, bij een installatie
die al draait. Werkt zolang er nog geen account bestaat.
--update Bijwerken naar de nieuwste versie --update Bijwerken naar de nieuwste versie
--uninstall Stoppen en verwijderen (je gegevens blijven staan) --uninstall Stoppen en verwijderen (je gegevens blijven staan)
--yes Niets vragen, overal ja op antwoorden --yes Niets vragen, overal ja op antwoorden
--dry-run Alleen tonen wat er zou gebeuren --dry-run Alleen tonen wat er zou gebeuren
--help Deze uitleg --help Deze uitleg
Het wachtwoord staat bewust niet als optie op de opdrachtregel: die is voor
iedereen op de server te zien met 'ps' en blijft in je shell-geschiedenis staan.
Voorbeelden: Voorbeelden:
sh install.sh sh install.sh
sh install.sh --bind 0.0.0.0 --port 8080 sh install.sh --bind 0.0.0.0 --port 8080
sh install.sh --admin ramon
SU_ADMIN_PASSWORD=... sh install.sh --admin ramon --yes
sh install.sh --update sh install.sh --update
UITLEG UITLEG
} }
@ -96,12 +116,18 @@ UITLEG
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
case "$1" in case "$1" in
--dir) DOEL="${2:?--dir vereist een pad}"; shift 2 ;; --dir) DOEL="${2:?--dir vereist een pad}"; shift 2 ;;
--port) POORT="${2:?--port vereist een nummer}"; shift 2 ;; --port) POORT="${2:?--port vereist een nummer}"; POORT_EXPLICIET=1; shift 2 ;;
--bind) BIND="${2:?--bind vereist een adres}"; shift 2 ;; --bind) BIND="${2:?--bind vereist een adres}"; BIND_EXPLICIET=1; shift 2 ;;
--branch) BRANCH="${2:?--branch vereist een naam}"; shift 2 ;; --branch) BRANCH="${2:?--branch vereist een naam}"; shift 2 ;;
--token) TOKEN="${2:?--token vereist een waarde}"; shift 2 ;; --token) TOKEN="${2:?--token vereist een waarde}"; shift 2 ;;
--admin) BEHEERDER="${2:?--admin vereist een gebruikersnaam}"; shift 2 ;;
--admin-password-file)
BEHEERDER_WW_BESTAND="${2:?--admin-password-file vereist een pad}"; shift 2 ;;
--admin-password)
fout "Gebruik --admin-password-file of SU_ADMIN_PASSWORD; een wachtwoord op de opdrachtregel is voor iedereen zichtbaar met 'ps'." ;;
--update) ACTIE="update"; shift ;; --update) ACTIE="update"; shift ;;
--uninstall) ACTIE="uninstall"; shift ;; --uninstall) ACTIE="uninstall"; shift ;;
--create-admin) ACTIE="create-admin"; shift ;;
--yes|-y) JA=1; shift ;; --yes|-y) JA=1; shift ;;
--dry-run) DROOG=1; shift ;; --dry-run) DROOG=1; shift ;;
--help|-h) gebruik; exit 0 ;; --help|-h) gebruik; exit 0 ;;
@ -110,6 +136,18 @@ while [ $# -gt 0 ]; do
done done
case "$POORT" in ''|*[!0-9]*) fout "Ongeldige poort: $POORT" ;; esac case "$POORT" in ''|*[!0-9]*) fout "Ongeldige poort: $POORT" ;; esac
[ "$POORT" -ge 1 ] && [ "$POORT" -le 65535 ] || fout "Poort buiten bereik: $POORT"
# Docker publiceert op een adres, niet op een hostnaam. Alleen cijfers, punten
# en dubbele punten (IPv6) toestaan, zodat een typefout hier opvalt en niet pas
# als 'docker compose up' er een onbegrijpelijke fout over geeft.
geldig_bind() {
case "$1" in
''|*[!0-9.:a-fA-F]*) return 1 ;;
*) return 0 ;;
esac
}
geldig_bind "$BIND" || fout "Ongeldig bind-adres: $BIND (verwacht een IP-adres, bijvoorbeeld 127.0.0.1 of 0.0.0.0)"
# ── Rechten ────────────────────────────────────────────────────────────────── # ── Rechten ──────────────────────────────────────────────────────────────────
# Alleen sudo gebruiken waar het echt nodig is: kan de gebruiker zelf al in de # Alleen sudo gebruiken waar het echt nodig is: kan de gebruiker zelf al in de
@ -146,6 +184,18 @@ controleer_systeem() {
} }
controleer_docker() { controleer_docker() {
# Bij een proefdraai niets aanroepen wat via sudo loopt: dan staat er
# ineens een wachtwoordvraag op het scherm terwijl we net beloofd hebben
# dat er niets gebeurt — en zonder terminal blijft hij daarop hangen.
if [ "$DROOG" -eq 1 ]; then
if heeft docker; then
goed "Docker aanwezig (de daemon wordt bij een proefdraai niet aangesproken)"
else
waarschuw "Docker is niet geïnstalleerd; het script zou aanbieden dat te doen."
fi
return 0
fi
if heeft docker; then if heeft docker; then
if $SUDO docker info >/dev/null 2>&1; then if $SUDO docker info >/dev/null 2>&1; then
goed "Docker draait ($($SUDO docker version --format '{{.Server.Version}}' 2>/dev/null || echo '?'))" goed "Docker draait ($($SUDO docker version --format '{{.Server.Version}}' 2>/dev/null || echo '?'))"
@ -216,18 +266,127 @@ haal_broncode() {
} }
# ── Instellingen ───────────────────────────────────────────────────────────── # ── Instellingen ─────────────────────────────────────────────────────────────
server_ip() { hostname -I 2>/dev/null | awk '{print $1}'; }
kies_bind() {
# Alleen vragen als er nog iets te kiezen valt: met --bind, met --yes of
# zonder terminal is de keuze al gemaakt.
[ "$BIND_EXPLICIET" -eq 1 ] && return 0
[ "$JA" -eq 1 ] && return 0
[ "$DROOG" -eq 1 ] && return 0
[ -e /dev/tty ] || return 0
# Installeer je opnieuw over een bestaande map, dan is je huidige keuze het
# uitgangspunt — anders zet enter je server ongemerkt terug op loopback.
huidig=""
if [ -f "$DOEL/.env" ]; then
lees_env_terug "$DOEL/.env"
huidig="$BIND"
fi
ip="$(server_ip)"
stap "Bereikbaarheid"
printf ' Waarop moet de webinterface luisteren?\n\n'
printf ' 1) Alleen deze server (127.0.0.1) — veiligst\n'
printf ' Erbij via een SSH-tunnel of een reverse proxy op deze machine.\n'
printf ' 2) Het hele netwerk (0.0.0.0)\n'
printf ' Direct bereikbaar op http://%s:%s — zet er wel TLS voor.\n' "${ip:-<server-ip>}" "$POORT"
printf ' 3) Een specifiek adres\n\n'
if [ -n "$huidig" ]; then
printf ' Nu ingesteld: %s\n' "$huidig"
printf ' Keuze [enter = zo laten]: '
else
printf ' Keuze [1]: '
fi
read -r keuze </dev/tty || keuze=""
case "$keuze" in
1) BIND="127.0.0.1" ;;
2) BIND="0.0.0.0" ;;
3) printf ' Adres: '
read -r adres </dev/tty || adres=""
geldig_bind "$adres" || fout "Ongeldig bind-adres: $adres"
BIND="$adres" ;;
'') BIND="${huidig:-127.0.0.1}" ;;
*) fout "Onbekende keuze: $keuze" ;;
esac
BIND_EXPLICIET=1
goed "Luistert op $BIND"
}
zet_env_regel() {
# zet_env_regel BESTAND SLEUTEL WAARDE — vervangt de regel of voegt hem toe.
# Waarden zijn hier altijd al gevalideerd (cijfers, punten, dubbele punten).
$SUDO sh -c "sed -i '/^$2=/d' '$1' && printf '%s=%s\n' '$2' '$3' >> '$1'"
}
lees_env_terug() {
# Wat er in .env staat is de waarheid, niet wat dit script als standaard
# heeft. Zonder dit klopt bij --update de wachtlus en het slotadres niet
# meer zodra je ooit een andere poort hebt gekozen.
[ -f "$1" ] || return 0
# Lezen kan meestal zonder sudo; alleen als het bestand van root is en
# afgeschermd, is de omweg nodig.
if [ -r "$1" ]; then
_lees="cat"
elif [ -n "$SUDO" ]; then
_lees="$SUDO cat"
elif heeft sudo; then
_lees="sudo cat"
else
_lees="cat"
fi
_inhoud="$($_lees "$1" 2>/dev/null || true)"
_poort="$(printf '%s\n' "$_inhoud" | sed -n 's/^PORT=//p' | tail -1)"
_bind="$(printf '%s\n' "$_inhoud" | sed -n 's/^BIND=//p' | tail -1)"
if [ "$POORT_EXPLICIET" -eq 0 ]; then
case "$_poort" in ''|*[!0-9]*) ;; *) POORT="$_poort" ;; esac
fi
if [ "$BIND_EXPLICIET" -eq 0 ] && [ -n "$_bind" ] && geldig_bind "$_bind"; then
BIND="$_bind"
fi
return 0
}
schrijf_env() { schrijf_env() {
stap "Instellingen" stap "Instellingen"
env_bestand="$DOEL/.env" env_bestand="$DOEL/.env"
if [ -f "$env_bestand" ]; then if [ -f "$env_bestand" ]; then
goed "Bestaande .env blijft ongemoeid" # Een bestaande .env bevat jouw instellingen; die blijven staan. Alleen
# BIND en PORT wel bijwerken als ze expliciet zijn meegegeven. # wat je nu expliciet meegeeft wordt doorgevoerd — dat is precies het
# verschil dat eerder ontbrak, waardoor --bind niets deed zodra er al
# een .env stond.
gewijzigd=0
if [ "$BIND_EXPLICIET" -eq 1 ]; then
if [ "$DROOG" -eq 1 ]; then
printf ' [droog] BIND=%s in bestaande .env zetten\n' "$BIND"
else
zet_env_regel "$env_bestand" BIND "$BIND"
fi
gewijzigd=1
fi
if [ "$POORT_EXPLICIET" -eq 1 ]; then
if [ "$DROOG" -eq 1 ]; then
printf ' [droog] PORT=%s in bestaande .env zetten\n' "$POORT"
else
zet_env_regel "$env_bestand" PORT "$POORT"
fi
gewijzigd=1
fi
if [ "$gewijzigd" -eq 1 ]; then
goed "Bestaande .env bijgewerkt (BIND=$BIND, PORT=$POORT)"
else
lees_env_terug "$env_bestand"
goed "Bestaande .env blijft ongemoeid (BIND=$BIND, PORT=$POORT)"
fi
return 0 return 0
fi fi
if [ "$DROOG" -eq 1 ]; then if [ "$DROOG" -eq 1 ]; then
printf ' [droog] .env aanmaken met BIND=%s PORT=%s\n' "$BIND" "$POORT" printf ' [droog] .env aanmaken met BIND=%s PORT=%s\n' "$BIND" "$POORT"
return 0 return 0
fi fi
if [ -f "$DOEL/.env.example" ]; then if [ -f "$DOEL/.env.example" ]; then
$SUDO cp "$DOEL/.env.example" "$env_bestand" $SUDO cp "$DOEL/.env.example" "$env_bestand"
else else
@ -276,6 +435,148 @@ wacht_op_gereed() {
return 1 return 1
} }
# ── Beheerdersaccount ────────────────────────────────────────────────────────
# Tussen "container draait" en "eerste account" ligt een venster waarin iedereen
# die de pagina bereikt zichzelf beheerder kan maken. Dat venster hoort zo kort
# mogelijk te zijn, dus kan het script het account meteen aanmaken.
WW_GEGENEREERD=0
BEHEERDER_WW_TOON=""
json_tekst() {
# Escapet een waarde voor gebruik tussen JSON-aanhalingstekens.
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
}
genereer_wachtwoord() {
LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom 2>/dev/null | head -c 24
}
vraag_wachtwoord() {
# Twee keer intypen, met de echo uit. Mislukt dat, dan valt hij terug op
# een gegenereerd wachtwoord in plaats van er een half account van te maken.
oude_stty=""
if stty -g </dev/tty >/dev/null 2>&1; then oude_stty="$(stty -g </dev/tty)"; fi
poging=0
while [ "$poging" -lt 3 ]; do
poging=$((poging + 1))
if [ -n "$oude_stty" ]; then stty -echo </dev/tty; fi
printf ' Wachtwoord voor %s (minstens 10 tekens): ' "$BEHEERDER"
read -r ww1 </dev/tty || ww1=""
printf '\n Nogmaals: '
read -r ww2 </dev/tty || ww2=""
if [ -n "$oude_stty" ]; then stty "$oude_stty" </dev/tty; fi
printf '\n'
if [ "$ww1" != "$ww2" ]; then
waarschuw "De twee invoeren verschillen."
elif [ "${#ww1}" -lt 10 ]; then
waarschuw "Minstens 10 tekens, dat is ook wat de server eist."
else
BEHEERDER_WW="$ww1"
return 0
fi
done
waarschuw "Geen bruikbaar wachtwoord ingevoerd; er wordt er een aangemaakt."
return 1
}
bepaal_wachtwoord() {
# Volgorde: bestand, omgevingsvariabele, vragen, anders zelf aanmaken.
[ -n "$BEHEERDER_WW" ] && return 0
if [ -n "$BEHEERDER_WW_BESTAND" ]; then
[ -r "$BEHEERDER_WW_BESTAND" ] || fout "Kan $BEHEERDER_WW_BESTAND niet lezen."
BEHEERDER_WW="$(head -n 1 "$BEHEERDER_WW_BESTAND")"
[ -n "$BEHEERDER_WW" ] || fout "$BEHEERDER_WW_BESTAND is leeg."
return 0
fi
if [ -n "${SU_ADMIN_PASSWORD:-}" ]; then
BEHEERDER_WW="$SU_ADMIN_PASSWORD"
return 0
fi
if [ "$JA" -eq 0 ] && [ -e /dev/tty ]; then
vraag_wachtwoord && return 0
fi
BEHEERDER_WW="$(genereer_wachtwoord)"
[ -n "$BEHEERDER_WW" ] || fout "Kon geen wachtwoord aanmaken (/dev/urandom niet leesbaar). Gebruik --admin-password-file."
WW_GEGENEREERD=1
return 0
}
vraag_beheerder() {
# Niets meegegeven, maar wel een terminal? Dan hier aanbieden, want dit is
# het moment waarop het uitmaakt.
[ -n "$BEHEERDER" ] && return 0
[ "$JA" -eq 1 ] && return 0
[ "$DROOG" -eq 1 ] && return 0
[ -e /dev/tty ] || return 0
stap "Beheerdersaccount"
if ! vraag "Nu meteen een beheerdersaccount aanmaken?"; then
return 0
fi
printf ' Gebruikersnaam: '
read -r naam </dev/tty || naam=""
naam="$(printf '%s' "$naam" | tr -d '[:space:]')"
if [ -z "$naam" ]; then
waarschuw "Geen naam opgegeven; overgeslagen."
return 0
fi
BEHEERDER="$naam"
return 0
}
beheerder_vooraf() {
# Alles wat om invoer vraagt gebeurt vóór het bouwen, zodat je daarna kunt
# weglopen terwijl het image gebouwd wordt.
[ "$DROOG" -eq 1 ] && return 0
vraag_beheerder
[ -n "$BEHEERDER" ] || return 0
bepaal_wachtwoord
return 0
}
maak_beheerder() {
[ -n "$BEHEERDER" ] || return 0
if [ "$DROOG" -eq 1 ]; then
stap "Beheerdersaccount"
printf ' [droog] account %s aanmaken via POST /api/auth/setup\n' "$BEHEERDER"
return 0
fi
bepaal_wachtwoord
# Het wachtwoord gaat via stdin naar curl, niet via de opdrachtregel:
# argumenten zijn voor elke gebruiker op de server zichtbaar in 'ps'.
antwoord="$(mktemp 2>/dev/null || echo /tmp/su-setup.$$)"
code="$(printf '{"username":"%s","password":"%s"}' \
"$(json_tekst "$BEHEERDER")" "$(json_tekst "$BEHEERDER_WW")" \
| curl -sS -o "$antwoord" -w '%{http_code}' \
-X POST -H 'Content-Type: application/json' \
--data-binary @- \
"http://127.0.0.1:${POORT}/api/auth/setup" 2>/dev/null || true)"
# Bij een mislukte verbinding schrijft curl zelf al "000"; kan curl helemaal
# niet draaien, dan blijft het leeg. Beide gevallen zijn hetzelfde verhaal.
case "$code" in ''|*[!0-9]*) code="000" ;; esac
case "$code" in
200) goed "Beheerder '$BEHEERDER' aangemaakt"
# Alleen een zelf aangemaakt wachtwoord moet nog getoond worden;
# een ingetypt of meegegeven wachtwoord kent de gebruiker al.
if [ "$WW_GEGENEREERD" -eq 1 ]; then
BEHEERDER_WW_TOON="$BEHEERDER_WW"
fi ;;
409) waarschuw "Er bestond al een account; er is niets gewijzigd."
BEHEERDER=""; WW_GEGENEREERD=0 ;;
000) waarschuw "Server Up was niet bereikbaar; account niet aangemaakt."
BEHEERDER=""; WW_GEGENEREERD=0 ;;
*) melding="$(sed -n 's/.*"msg" *: *"\([^"]*\)".*/\1/p' "$antwoord" 2>/dev/null)"
waarschuw "Aanmaken mislukt (HTTP $code)${melding:+: }${melding:-}"
BEHEERDER=""; WW_GEGENEREERD=0 ;;
esac
rm -f "$antwoord"
BEHEERDER_WW=""
}
afsluiting() { afsluiting() {
if [ "$BIND" = "0.0.0.0" ]; then if [ "$BIND" = "0.0.0.0" ]; then
ip="$(hostname -I 2>/dev/null | awk '{print $1}')" ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
@ -288,9 +589,18 @@ afsluiting() {
printf '%s Server Up draait op %s%s\n' "$C_OK" "$adres" "$C_UIT" printf '%s Server Up draait op %s%s\n' "$C_OK" "$adres" "$C_UIT"
printf '%s────────────────────────────────────────────────────────%s\n\n' "$C_KOP" "$C_UIT" printf '%s────────────────────────────────────────────────────────%s\n\n' "$C_KOP" "$C_UIT"
printf ' %sMaak nu meteen een beheerdersaccount aan.%s\n' "$C_WAARSCHUW" "$C_UIT" if [ -n "$BEHEERDER" ]; then
printf ' Zolang dat niet gebeurd is, kan iedereen die deze pagina\n' printf ' %s✔%s Inloggen als: %s%s%s\n' "$C_OK" "$C_UIT" "$C_KOP" "$BEHEERDER" "$C_UIT"
printf ' bereikt het account claimen.\n\n' if [ "$WW_GEGENEREERD" -eq 1 ]; then
printf ' Wachtwoord: %s%s%s\n' "$C_KOP" "$BEHEERDER_WW_TOON" "$C_UIT"
printf ' %sSchrijf dit nu over — het staat nergens anders.%s\n' "$C_WAARSCHUW" "$C_UIT"
fi
printf '\n'
else
printf ' %sMaak nu meteen een beheerdersaccount aan.%s\n' "$C_WAARSCHUW" "$C_UIT"
printf ' Zolang dat niet gebeurd is, kan iedereen die deze pagina\n'
printf ' bereikt het account claimen.\n\n'
fi
if [ "$BIND" = "127.0.0.1" ]; then if [ "$BIND" = "127.0.0.1" ]; then
printf ' Alleen bereikbaar vanaf deze server zelf. Van buitenaf erbij?\n' printf ' Alleen bereikbaar vanaf deze server zelf. Van buitenaf erbij?\n'
@ -326,6 +636,13 @@ verwijderen() {
} }
# ── Hoofdprogramma ─────────────────────────────────────────────────────────── # ── Hoofdprogramma ───────────────────────────────────────────────────────────
# De testsuite laadt dit bestand om losse functies te kunnen aanroepen — vooral
# de vragen die bepalen waarop de server straks luistert. Zonder deze uitgang
# zou het script bij het inladen meteen een hele installatie beginnen.
if [ -n "${SU_INSTALL_ALLEEN_FUNCTIES:-}" ]; then
return 0 2>/dev/null || exit 0
fi
printf '\n%sServer Up — installatiescript %s%s\n' "$C_KOP" "$SCRIPT_VERSIE" "$C_UIT" printf '\n%sServer Up — installatiescript %s%s\n' "$C_KOP" "$SCRIPT_VERSIE" "$C_UIT"
[ "$DROOG" -eq 1 ] && waarschuw "Proefdraai: er wordt niets gewijzigd." [ "$DROOG" -eq 1 ] && waarschuw "Proefdraai: er wordt niets gewijzigd."
@ -339,18 +656,45 @@ case "$ACTIE" in
controleer_systeem controleer_systeem
controleer_docker controleer_docker
haal_broncode haal_broncode
# Ook bij bijwerken: expliciete --bind/--port doorvoeren, en anders
# overnemen wat er al staat.
schrijf_env
start start
wacht_op_gereed || true wacht_op_gereed || true
printf '\n' printf '\n'
goed "Bijgewerkt." goed "Bijgewerkt."
;; ;;
create-admin)
# Losse actie voor een installatie die al draait: handig als het
# aanmaken tijdens de installatie is misgegaan, of overgeslagen.
# Docker en schrijfrechten zijn hier niet nodig — alleen .env lezen
# en één verzoek naar de draaiende server — dus geen bepaal_rechten.
lees_env_terug "$DOEL/.env"
if [ -z "$BEHEERDER" ]; then
if [ -e /dev/tty ] && [ "$JA" -eq 0 ]; then
printf '\n Gebruikersnaam: '
read -r naam </dev/tty || naam=""
BEHEERDER="$(printf '%s' "$naam" | tr -d '[:space:]')"
fi
[ -n "$BEHEERDER" ] || fout "Geef de gebruikersnaam op met --admin NAAM."
fi
stap "Beheerdersaccount"
maak_beheerder
if [ -n "$BEHEERDER" ] && [ "$WW_GEGENEREERD" -eq 1 ]; then
printf '\n Wachtwoord: %s%s%s\n' "$C_KOP" "$BEHEERDER_WW_TOON" "$C_UIT"
printf ' %sSchrijf dit nu over — het staat nergens anders.%s\n\n' "$C_WAARSCHUW" "$C_UIT"
fi
;;
install) install)
controleer_systeem controleer_systeem
controleer_docker controleer_docker
kies_bind
beheerder_vooraf
haal_broncode haal_broncode
schrijf_env schrijf_env
start start
wacht_op_gereed || true wacht_op_gereed || true
maak_beheerder
afsluiting afsluiting
;; ;;
esac esac

View file

@ -51,7 +51,8 @@ def test_help_werkt_en_noemt_alle_opties():
r = _draai("--help") r = _draai("--help")
assert r.returncode == 0 assert r.returncode == 0
for optie in ("--dir", "--port", "--bind", "--branch", "--token", for optie in ("--dir", "--port", "--bind", "--branch", "--token",
"--update", "--uninstall", "--yes", "--dry-run"): "--update", "--uninstall", "--yes", "--dry-run",
"--admin", "--admin-password-file", "--create-admin"):
assert optie in r.stdout, f"{optie} niet gedocumenteerd in --help" assert optie in r.stdout, f"{optie} niet gedocumenteerd in --help"
@ -125,6 +126,382 @@ def test_proefdraai_werkt_zonder_curl_en_tar(tmp_path):
assert not (tmp_path / "su").exists() assert not (tmp_path / "su").exists()
# ── Beheerdersaccount ────────────────────────────────────────────────────────
def test_wachtwoord_mag_niet_op_de_opdrachtregel():
"""Argumenten zijn voor elke gebruiker op de server zichtbaar met 'ps' en
blijven in de shell-geschiedenis staan."""
r = _draai("--admin-password", "geheimgeheim")
assert r.returncode != 0
assert "ps" in r.stderr
def test_create_admin_zonder_naam_stopt(tmp_path):
r = _draai("--create-admin", "--dir", str(tmp_path), "--yes")
assert r.returncode != 0
assert "--admin" in r.stderr
def test_dry_run_toont_het_aanmaken_van_het_account(tmp_path):
r = _draai("--dry-run", "--dir", str(tmp_path / "su"), "--yes", "--admin", "ramon")
assert "/api/auth/setup" in r.stdout
assert "ramon" in r.stdout
def test_wachtwoord_staat_niet_in_de_curl_argumenten():
"""Het wachtwoord gaat via stdin naar curl (--data-binary @-), niet als
argument. Anders is het alsnog zichtbaar in 'ps'."""
tekst = SCRIPT.read_text(encoding="utf-8")
setup = tekst[tekst.index("maak_beheerder()"):]
setup = setup[:setup.index("\nafsluiting()")]
assert "--data-binary @-" in setup
assert not re.search(r'-d\s+["\']?\{', setup), "JSON als curl-argument"
@pytest.mark.parametrize("adres", ["localhost", "0.0.0.0;rm -rf /", "mijn server"])
def test_ongeldig_bind_adres_stopt(adres):
r = _draai("--bind", adres)
assert r.returncode != 0
assert "bind" in r.stderr.lower()
@pytest.mark.parametrize("adres", ["127.0.0.1", "0.0.0.0", "10.0.20.22", "::1"])
def test_geldig_bind_adres_wordt_geaccepteerd(adres, tmp_path):
r = _draai("--dry-run", "--dir", str(tmp_path / "su"), "--yes", "--bind", adres)
assert r.returncode == 0, r.stderr
assert f"BIND={adres}" in r.stdout
# ── De vragen die het script stelt ───────────────────────────────────────────
# Interactief is de normale manier waarop iemand dit script draait, en juist
# daar bepaalt een verkeerd antwoord waar de server straks op luistert. Daarom
# worden de vragen hier echt gesteld, via een pseudo-terminal.
def _in_terminal(fragment, invoer, tmp_path, tijdslimiet=20):
"""Draait een stukje sh dat install.sh inlaadt, met een echte terminal."""
import os
import pty
import re as _re
import select
import signal
import time
script = tmp_path / "fragment.sh"
script.write_text(f"set --\nSU_INSTALL_ALLEEN_FUNCTIES=1 . {SCRIPT}\n{fragment}\n")
pid, fd = pty.fork()
if pid == 0: # kindproces
os.execvp("sh", ["sh", str(script)])
os.write(fd, invoer.encode())
uit, einde = b"", time.time() + tijdslimiet
while time.time() < einde:
klaar, _, _ = select.select([fd], [], [], 0.5)
if klaar:
try:
blok = os.read(fd, 4096)
except OSError: # terminal dicht: proces weg
break
if not blok:
break
uit += blok
elif os.waitpid(pid, os.WNOHANG)[0]:
break
try:
os.kill(pid, signal.SIGKILL)
except ProcessLookupError:
pass
try:
os.waitpid(pid, 0)
except ChildProcessError:
pass
return _re.sub(r"\x1b\[[0-9;]*m", "", uit.decode(errors="replace").replace("\r", ""))
BIND_FRAGMENT = ('DROOG=0; JA=0; BIND_EXPLICIET=0; DOEL="$MAP"\n'
'kies_bind\n'
'printf "UITKOMST BIND=%s\\n" "$BIND"\n')
@pytest.mark.parametrize("invoer,verwacht", [
("1\n", "127.0.0.1"),
("2\n", "0.0.0.0"),
("3\n10.0.20.99\n", "10.0.20.99"),
("\n", "127.0.0.1"), # enter zonder bestaande .env
])
def test_keuzemenu_voor_het_bind_adres(invoer, verwacht, tmp_path):
leeg = tmp_path / "leeg"
leeg.mkdir()
uit = _in_terminal(f'MAP="{leeg}"\n' + BIND_FRAGMENT, invoer, tmp_path)
assert f"UITKOMST BIND={verwacht}" in uit, uit
def test_enter_behoudt_de_bestaande_instelling(tmp_path):
"""Opnieuw installeren over een bestaande map mag je server niet ongemerkt
terugzetten op loopback."""
bestaand = tmp_path / "bestaand"
bestaand.mkdir()
(bestaand / ".env").write_text("BIND=10.0.20.5\nPORT=8080\n")
uit = _in_terminal(f'MAP="{bestaand}"\n' + BIND_FRAGMENT, "\n", tmp_path)
assert "UITKOMST BIND=10.0.20.5" in uit, uit
assert "Nu ingesteld: 10.0.20.5" in uit
@pytest.mark.parametrize("invoer,melding", [
("9\n", "Onbekende keuze"),
("3\nlocalhost\n", "Ongeldig bind-adres"),
])
def test_menu_weigert_onzin(invoer, melding, tmp_path):
leeg = tmp_path / "leeg"
leeg.mkdir()
uit = _in_terminal(f'MAP="{leeg}"\n' + BIND_FRAGMENT, invoer, tmp_path)
assert melding in uit
assert "UITKOMST" not in uit
WW_FRAGMENT = ('DROOG=0; JA=0; BEHEERDER=""\n'
'beheerder_vooraf\n'
'printf "UITKOMST naam=%s lengte=%s zelf=%s\\n" '
'"$BEHEERDER" "${#BEHEERDER_WW}" "$WW_GEGENEREERD"\n')
def test_account_aanmaken_kan_geweigerd_worden(tmp_path):
uit = _in_terminal(WW_FRAGMENT, "n\n", tmp_path)
assert "UITKOMST naam= lengte=0 zelf=0" in uit
def test_wachtwoord_twee_keer_intypen(tmp_path):
uit = _in_terminal(WW_FRAGMENT, "j\nramon\neenlangwachtwoord\neenlangwachtwoord\n", tmp_path)
assert "UITKOMST naam=ramon lengte=17 zelf=0" in uit, uit
@pytest.mark.parametrize("invoer,melding", [
("j\nramon\neenlangwachtwoord\nietsanders\neenlangwachtwoord\neenlangwachtwoord\n",
"verschillen"),
("j\nramon\nkort\nkort\neenlangwachtwoord\neenlangwachtwoord\n",
"Minstens 10 tekens"),
])
def test_wachtwoord_mag_opnieuw_na_een_fout(invoer, melding, tmp_path):
uit = _in_terminal(WW_FRAGMENT, invoer, tmp_path)
assert melding in uit
assert "UITKOMST naam=ramon lengte=17 zelf=0" in uit, uit
def test_na_drie_pogingen_maakt_het_script_er_zelf_een(tmp_path):
"""Blijven hangen op een prompt is erger dan een gegenereerd wachtwoord."""
uit = _in_terminal(WW_FRAGMENT, "j\nramon\na\na\nb\nb\nc\nc\n", tmp_path)
assert "UITKOMST naam=ramon lengte=24 zelf=1" in uit, uit
def test_lege_gebruikersnaam_slaat_over(tmp_path):
uit = _in_terminal(WW_FRAGMENT, "j\n\n", tmp_path)
assert "Geen naam opgegeven" in uit
assert "UITKOMST naam= lengte=0 zelf=0" in uit
def test_proefdraai_vraagt_niets_en_gebruikt_geen_sudo(tmp_path):
"""Een proefdraai beloofde dat er niets gebeurt, maar riep wél 'sudo docker'
aan. Op een server waar sudo een wachtwoord wil, liep hij daarop vast."""
uit = _in_terminal(
f'DROOG=1; SUDO="sudo"; DOEL="{tmp_path}"\ncontroleer_docker\nprintf "KLAAR\\n"\n',
"", tmp_path, tijdslimiet=15)
assert "KLAAR" in uit, f"controleer_docker liep vast:\n{uit}"
assert "password" not in uit.lower()
# ── .env ─────────────────────────────────────────────────────────────────────
def test_bind_wordt_doorgevoerd_in_een_bestaande_env(tmp_path):
"""Eerder bleef een bestaande .env altijd ongemoeid, ook als je --bind
meegaf. Dan leek de installatie te lukken maar bleef de server onbereikbaar
op het oude adres."""
doel = tmp_path / "su"
doel.mkdir()
(doel / ".env").write_text("BIND=127.0.0.1\nPORT=5000\nSU_TAG=0.7.30\n")
r = _draai("--dry-run", "--dir", str(doel), "--yes", "--bind", "0.0.0.0")
assert "BIND=0.0.0.0 in bestaande .env zetten" in r.stdout
def test_env_blijft_ongemoeid_zonder_expliciete_opties(tmp_path):
"""Anders zet een --update je instellingen elke keer terug."""
doel = tmp_path / "su"
doel.mkdir()
(doel / ".env").write_text("BIND=0.0.0.0\nPORT=8080\n")
r = _draai("--dry-run", "--dir", str(doel), "--yes")
assert "ongemoeid" in r.stdout
# En hij rekent verder met wat er écht staat, niet met de standaardwaarden.
assert "BIND=0.0.0.0, PORT=8080" in r.stdout
def test_poort_uit_env_wordt_gebruikt_voor_het_slotadres(tmp_path):
doel = tmp_path / "su"
doel.mkdir()
(doel / ".env").write_text("BIND=0.0.0.0\nPORT=8080\n")
r = _draai("--dry-run", "--dir", str(doel), "--yes")
assert ":8080" in r.stdout
# ── Tegen een echt draaiende server ──────────────────────────────────────────
# Deze tests starten de app zelf en laten het script er een account op
# aanmaken. Dat is het enige wat aantoont dat de JSON-opbouw, de poortdetectie
# en de foutafhandeling in samenhang kloppen.
geen_curl = pytest.mark.skipif(not shutil.which("curl"),
reason="curl ontbreekt (o.a. in python:3.12-slim)")
def _vrije_poort():
import socket
s = socket.socket()
s.bind(("127.0.0.1", 0))
poort = s.getsockname()[1]
s.close()
return poort
@pytest.fixture
def draaiende_app(tmp_path):
"""De echte app op een vrije poort, met een lege configuratie."""
import os
import sys
import time
import urllib.error
import urllib.request
poort = _vrije_poort()
data = tmp_path / "data"
data.mkdir()
omgeving = dict(os.environ,
SU_CONFIG=str(data / "config.json"),
SU_AUDIT=str(data / "audit.db"),
SU_GIT_CACHE=str(data / "git"),
SU_SECRET=str(data / "secret.key"),
LIBRARY_DIR=str(data / "lib"),
DATA_DIR=str(tmp_path / "appdata"),
BACKUP_DIR=str(tmp_path / "backups"),
PORT=str(poort))
proces = subprocess.Popen([sys.executable, str(WORTEL / "server-up" / "app.py")],
env=omgeving, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True)
try:
for _ in range(80):
if proces.poll() is not None:
pytest.skip(f"app start niet: {proces.communicate()[0][-500:]}")
try:
urllib.request.urlopen(f"http://127.0.0.1:{poort}/healthz", timeout=1)
break
except (urllib.error.URLError, OSError):
time.sleep(0.25)
else:
pytest.skip("app reageerde niet op tijd")
yield poort
finally:
proces.terminate()
try:
proces.wait(timeout=10)
except subprocess.TimeoutExpired:
proces.kill()
def _inloggen(poort, naam, wachtwoord):
import json
import urllib.error
import urllib.request
verzoek = urllib.request.Request(
f"http://127.0.0.1:{poort}/api/auth/login",
data=json.dumps({"username": naam, "password": wachtwoord}).encode(),
headers={"Content-Type": "application/json"})
try:
with urllib.request.urlopen(verzoek, timeout=5) as a:
return a.status
except urllib.error.HTTPError as e:
return e.code
@geen_curl
def test_script_maakt_een_werkend_beheerdersaccount(draaiende_app, tmp_path):
poort = draaiende_app
inst = tmp_path / "inst"
inst.mkdir()
# De poort komt uit .env, niet uit een optie: zo werkt het ook als je
# destijds met --port hebt geïnstalleerd.
(inst / ".env").write_text(f"BIND=127.0.0.1\nPORT={poort}\n")
ww = tmp_path / "ww"
ww.write_text("een-heel-lang-wachtwoord\n")
r = _draai("--create-admin", "--dir", str(inst), "--admin", "ramon",
"--admin-password-file", str(ww), "--yes")
assert r.returncode == 0, r.stderr
assert "aangemaakt" in r.stdout
assert _inloggen(poort, "ramon", "een-heel-lang-wachtwoord") == 200
@geen_curl
def test_wachtwoord_met_aanhalingstekens_en_backslashes(draaiende_app, tmp_path):
"""Het wachtwoord gaat als JSON over de lijn; zonder escapen levert dit een
kapot verzoek op of erger een ander wachtwoord dan je denkt."""
poort = draaiende_app
inst = tmp_path / "inst"
inst.mkdir()
(inst / ".env").write_text(f"PORT={poort}\n")
lastig = 'pa"ss\\wo/rd$`{}[]&|;<>#!\'x'
ww = tmp_path / "ww"
ww.write_text(lastig + "\n")
r = _draai("--create-admin", "--dir", str(inst), "--admin", "lastig",
"--admin-password-file", str(ww), "--yes")
assert r.returncode == 0, r.stderr
assert "aangemaakt" in r.stdout
assert _inloggen(poort, "lastig", lastig) == 200
@geen_curl
def test_zelf_aangemaakt_wachtwoord_wordt_getoond_en_werkt(draaiende_app, tmp_path):
"""Zonder bestand, omgevingsvariabele of terminal maakt het script er zelf
één aan. Die moet dan wel op het scherm komen, want hij staat nergens."""
poort = draaiende_app
inst = tmp_path / "inst"
inst.mkdir()
(inst / ".env").write_text(f"PORT={poort}\n")
r = _draai("--create-admin", "--dir", str(inst), "--admin", "auto", "--yes")
assert r.returncode == 0, r.stderr
getoond = re.search(r"Wachtwoord: (\S+)", r.stdout)
assert getoond, f"geen wachtwoord getoond in:\n{r.stdout}"
assert _inloggen(poort, "auto", getoond.group(1)) == 200
@geen_curl
def test_tweede_account_wordt_netjes_geweigerd(draaiende_app, tmp_path):
poort = draaiende_app
inst = tmp_path / "inst"
inst.mkdir()
(inst / ".env").write_text(f"PORT={poort}\n")
ww = tmp_path / "ww"
ww.write_text("een-heel-lang-wachtwoord\n")
_draai("--create-admin", "--dir", str(inst), "--admin", "eerste",
"--admin-password-file", str(ww), "--yes")
r = _draai("--create-admin", "--dir", str(inst), "--admin", "tweede",
"--admin-password-file", str(ww), "--yes")
assert r.returncode == 0, r.stderr
assert "bestond al" in r.stdout
assert _inloggen(poort, "tweede", "een-heel-lang-wachtwoord") != 200
@geen_curl
def test_onbereikbare_server_meldt_dat_zonder_te_ontsporen(tmp_path):
inst = tmp_path / "inst"
inst.mkdir()
(inst / ".env").write_text(f"PORT={_vrije_poort()}\n")
ww = tmp_path / "ww"
ww.write_text("een-heel-lang-wachtwoord\n")
r = _draai("--create-admin", "--dir", str(inst), "--admin", "ramon",
"--admin-password-file", str(ww), "--yes")
assert r.returncode == 0, r.stderr
assert "niet bereikbaar" in r.stdout
def test_verwijst_naar_het_juiste_git_domein(): def test_verwijst_naar_het_juiste_git_domein():
tekst = SCRIPT.read_text(encoding="utf-8") tekst = SCRIPT.read_text(encoding="utf-8")
assert 'GIT_HOST="git.ramonbesselink.nl"' in tekst assert 'GIT_HOST="git.ramonbesselink.nl"' in tekst