server-up/tests/conftest.py
Ramon 916e54a229 v0.5.00-beta - authenticatie + beveiligingsfixes
Server Up beheerde de Docker-daemon als root zonder enige vorm van
authenticatie: elke /api/*-route was gelijk aan root-toegang op de host.

- Authenticatie toegevoegd (core/auth.py): lokale accounts met scrypt-hash,
  sessiecookie (HttpOnly, SameSite=Strict) en optionele trusted-proxy-header
  SSO die alleen vanaf geconfigureerde proxy-IP's wordt vertrouwd.
- before_request-guard schermt alle API-routes af; loginscherm en
  eerste-account-setup in de UI.
- CSRF-token verplicht op elke mutatie; GET-varianten van state-wijzigende
  routes verwijderd (o.a. /api/docker/restart was via <img> te triggeren).
- Path traversal in /api/store/install gedicht; gedeelde safe_name()-validatie
  voor stack-, instantie- en repo-namen.
- Git-tokens worden niet meer teruggegeven via /api/repos en /api/settings
  (has_token-vlag); settings-PUT wist een bestaand token niet meer en kan AUTH
  niet overschrijven.
- Git-URL's beperkt tot http(s)/ssh/scp-syntax; ext::-transport (voert een
  shell-commando uit) en file:// worden geweigerd.
- Boilerplate-templates renderen in een SandboxedEnvironment (SSTI).
- Automatisch syncen van repo's bij boot standaard uit (AUTO_SYNC_ON_BOOT),
  optionele commit-pinning per repo.
- Waitress in plaats van de Flask-ontwikkelserver, MAX_CONTENT_LENGTH,
  ProxyFix, en CSP/X-Frame-Options/nosniff/Referrer-Policy headers.
- Front-end libraries (Tailwind, Alpine, htmx) lokaal meegeleverd i.p.v. CDN;
  Google Fonts verwijderd. Werkt nu ook offline.
- SSH host-key-verificatie aan (accept-new + /data/known_hosts).
- Lichte /healthz voor de healthcheck i.p.v. `docker info`.
- config.json en secret.key met 0600-rechten.
- Poort standaard op 127.0.0.1 gebonden.
- Audit-log gebruikt één gedeelde SQLite-verbinding (fd-lek per job verholpen);
  joblogs afgekapt op 2000 regels.
- VERSION-bestand is de enige bron voor het versienummer.
- pytest-suite toegevoegd (74 tests) en als stap in beide deploy-workflows.
- fix-config.sh verwijderd (hardgecodeerd intern IP, overschreef config).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb
2026-07-26 14:40:05 +02:00

61 lines
2.1 KiB
Python

"""Gedeelde fixtures. Elke test krijgt een eigen /data-map, zodat de suite
nooit de echte config.json, audit-db of git-cache van een draaiende instantie
raakt."""
import importlib
import sys
from pathlib import Path
import pytest
APP_DIR = Path(__file__).resolve().parent.parent / "server-up"
if str(APP_DIR) not in sys.path:
sys.path.insert(0, str(APP_DIR))
@pytest.fixture
def env(tmp_path, monkeypatch):
"""Verse omgeving: config, audit-db, git-cache en secret in tmp_path."""
data = tmp_path / "data"
data.mkdir()
lib = tmp_path / "stacks"
lib.mkdir()
monkeypatch.setenv("SU_CONFIG", str(data / "config.json"))
monkeypatch.setenv("SU_AUDIT", str(data / "audit.db"))
monkeypatch.setenv("SU_GIT_CACHE", str(data / "git"))
monkeypatch.setenv("SU_SECRET", str(data / "secret.key"))
monkeypatch.setenv("LIBRARY_DIR", str(lib))
monkeypatch.setenv("DATA_DIR", str(tmp_path / "appdata"))
monkeypatch.setenv("BACKUP_DIR", str(tmp_path / "backups"))
# Modules opnieuw importeren zodat ze de nieuwe paden oppikken.
for name in list(sys.modules):
if name == "core" or name.startswith("core.") or name == "app":
del sys.modules[name]
core = importlib.import_module("core")
core._path = Path(data / "config.json")
return {"data": data, "lib": lib, "tmp": tmp_path, "core": core}
@pytest.fixture
def client(env, monkeypatch):
"""Flask-testclient met een aangemaakt account en geldige sessie."""
import app as app_module
app_module.app.config["TESTING"] = True
c = app_module.app.test_client()
return c
@pytest.fixture
def anon_client(env):
"""Testclient zonder sessie."""
import app as app_module
app_module.app.config["TESTING"] = True
return app_module.app.test_client()
def login(client, username="tester", password="hunter2hunter2"):
"""Maak het eerste account aan en retourneer het CSRF-token."""
r = client.post("/api/auth/setup",
json={"username": username, "password": password})
assert r.status_code == 200, r.get_json()
return r.get_json()["csrf_token"]