diff --git a/CHANGELOG.md b/CHANGELOG.md index db157c9..ad17957 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +# v0.8.22-beta — UniFi leest zijn databasegegevens opnieuw in + +Het linuxserver-image schrijft `/config/data/system.properties` alleen bij de +allereerste start ("Only evaluated on first run"). Alles wat daarna in Compose +verandert — de toegevoegde `authSource=admin`, een ander wachtwoord — kwam daar +dus nooit in terecht. UniFi bleef verbinden met de gegevens van de allereerste +installatie, en MongoDB antwoordde met `UserNotFound: unifi@unifi`. + +- **De databasegegevens worden bij elke start gelijkgetrokken.** De + reparatiecontainer zet `db.mongo.uri`, `statdb.mongo.uri`, `db.mongo.local` + en `unifi.db.name` opnieuw uit de actuele Compose-omgeving. Andere + instellingen in het bestand blijven staan. +- **Bijzondere tekens gaan ge-url-codeerd de URI in.** Een `@` of `/` in het + wachtwoord brak de verbindingsreeks anders doormidden. +- **Het bestand houdt zijn eigenaar en rechten.** Het wordt op zijn plaats + overschreven, zodat UniFi er zelf in kan blijven schrijven. +- **Het oude wachtwoord telt mee bij de reparatie.** Wijkt `.env` af van + waarmee de database ooit is aangemaakt, dan meldt de reparatie zich aan met + het wachtwoord uit `system.properties` en trekt daarna alles gelijk — in + plaats van je met een dode stack achter te laten. + # v0.8.21-beta — Wijzigingen blijven staan, UniFi komt online Een geslaagde instellingenwijziging werd meteen weer teruggedraaid: het diff --git a/VERSION b/VERSION index 88e6b40..b553cab 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.8.21-beta +0.8.22-beta diff --git a/apps/unifi-network/files/compose.yaml b/apps/unifi-network/files/compose.yaml index ade0e5f..4f8771b 100644 --- a/apps/unifi-network/files/compose.yaml +++ b/apps/unifi-network/files/compose.yaml @@ -70,6 +70,9 @@ services: - MONGO_AUTHSOURCE=admin volumes: - ./init-mongo.sh:/init-mongo.sh:ro + # Dezelfde map als UniFi zelf: hierin staat system.properties, dat het + # image alleen bij de allereerste start schrijft. + - << appdata_dir >>/<< service_name >>/config:/unifi-config entrypoint: ["/bin/bash", "/init-mongo.sh"] depends_on: << service_name >>-mongo: diff --git a/apps/unifi-network/files/init-mongo.sh b/apps/unifi-network/files/init-mongo.sh index 3025451..444a290 100755 --- a/apps/unifi-network/files/init-mongo.sh +++ b/apps/unifi-network/files/init-mongo.sh @@ -17,7 +17,31 @@ if [[ -n "${MONGO_HOST:-}" ]]; then ) fi +props="${UNIFI_CONFIG_DIR:-/unifi-config}/data/system.properties" + +# UniFi bewaart in system.properties het wachtwoord waarmee de database ooit is +# aangemaakt. Wijkt dat af van wat er nu in .env staat, dan is dít het enige +# wachtwoord waarmee we nog binnenkomen — en dus de enige weg om het weer +# gelijk te trekken in plaats van de gebruiker met een dode stack te laten. +oud_wachtwoord="" +if [[ -f "${props}" ]]; then + oude_uri="$(grep -m1 '^db\.mongo\.uri=' "${props}" | cut -d= -f2- || true)" + oude_inlog="${oude_uri#mongodb://}" + oude_inlog="${oude_inlog%%@*}" + # Zonder @ staat er geen inlog in de URI; dan valt er ook niets te lezen. + if [[ "${oude_uri}" == *@* && "${oude_inlog}" == *:* ]]; then + oud_wachtwoord="${oude_inlog#*:}" + # %40 en vrienden weer terug naar het echte teken. + oud_wachtwoord="$(printf '%b' "${oud_wachtwoord//%/\\x}")" + fi +fi +# Aanhalingstekens en backslashes zouden de JavaScript-tekenreeks hieronder +# openbreken. +oud_wachtwoord="${oud_wachtwoord//\\/\\\\}" +oud_wachtwoord="${oud_wachtwoord//\"/\\\"}" + "${mongo_init_bin}" "${mongo_args[@]}" < naam && alle.indexOf(naam) === i); +// Het wachtwoord uit .env, en dat waarmee UniFi de database ooit heeft +// aangemaakt. Werkt alleen dat laatste nog, dan trekken we het hieronder +// alsnog gelijk. +const wachtwoorden = [beheerWachtwoord, oudWachtwoord] + .filter((ww, i, alle) => ww && alle.indexOf(ww) === i); function aanmelden() { for (const naam of kandidaten) { - try { - if (adminDb.auth(naam, beheerWachtwoord)) { - return naam; + for (const wachtwoord of wachtwoorden) { + try { + if (adminDb.auth(naam, wachtwoord)) { + return naam; + } + } catch (e) { + // Onbekende gebruiker of verkeerd wachtwoord: volgende combinatie. } - } catch (e) { - // Onbekende gebruiker of verkeerd wachtwoord: volgende kandidaat. } } try { @@ -102,4 +133,49 @@ if (authBron !== appDatabase) { ensureUser(appDatabase); } EOF + +# ── UniFi's eigen kopie van de databasegegevens ────────────────────────────── +# Het image schrijft /config/data/system.properties alleen als dat bestand er +# nog niet is ("Only evaluated on first run"). Alles wat daarna in compose +# verandert — een authSource erbij, een ander wachtwoord — komt er dus nooit in +# en UniFi blijft met de oude gegevens verbinden. Daarom zetten we de vier +# regels die over de database gaan bij elke start opnieuw goed. +if [[ -f "${props}" ]]; then + # De waarden gaan in een URI; een teken als @ of / zou die uit elkaar trekken. + urlcodeer() { + local invoer="$1" uit="" teken + for ((i = 0; i < ${#invoer}; i++)); do + teken="${invoer:i:1}" + case "${teken}" in + [a-zA-Z0-9.~_-]) uit+="${teken}" ;; + *) uit+="$(printf '%%%02X' "'${teken}")" ;; + esac + done + printf '%s' "${uit}" + } + + gebruiker="$(urlcodeer "${MONGO_USER}")" + wachtwoord="$(urlcodeer "${MONGO_PASS}")" + basis="mongodb://${gebruiker}:${wachtwoord}@${MONGO_HOST}:${MONGO_PORT:-27017}" + achtervoegsel="?tls=false" + if [[ -n "${MONGO_AUTHSOURCE:-}" ]]; then + achtervoegsel="${achtervoegsel}&authSource=${MONGO_AUTHSOURCE}" + fi + + tijdelijk="$(mktemp)" + grep -v -E '^(db\.mongo\.local|db\.mongo\.uri|statdb\.mongo\.uri|unifi\.db\.name)=' \ + "${props}" > "${tijdelijk}" || true + { + echo "db.mongo.local=false" + echo "db.mongo.uri=${basis}/${MONGO_DBNAME}${achtervoegsel}" + echo "statdb.mongo.uri=${basis}/${MONGO_DBNAME}_stat${achtervoegsel}" + echo "unifi.db.name=${MONGO_DBNAME}" + } >> "${tijdelijk}" + # Via cat en niet via mv of sed -i: zo houdt het bestand zijn inode, en dus + # zijn eigenaar en rechten. Anders is het ineens van root en kan UniFi zelf + # niets meer wegschrijven. + cat "${tijdelijk}" > "${props}" + rm -f "${tijdelijk}" + echo "system.properties bijgewerkt met de huidige databasegegevens." +fi <% endraw %> diff --git a/tests/test_apps.py b/tests/test_apps.py index 5b3f531..7efe429 100644 --- a/tests/test_apps.py +++ b/tests/test_apps.py @@ -446,6 +446,110 @@ def test_unifi_heeft_pi_geschikte_en_correct_geinitialiseerde_mongodb(): assert bp.metadata(app)["architectures"] == ["amd64", "arm64"] assert bp.metadata(app)["web"] == {"scheme": "https", "port": 8443} + +def test_unifi_zet_zijn_system_properties_bij_elke_start_goed(tmp_path): + """Het image schrijft /config/data/system.properties alleen bij de eerste + start ("Only evaluated on first run"). Een later toegevoegde authSource of + een gewijzigd wachtwoord kwam daar dus nooit in, en UniFi bleef met de oude + gegevens verbinden — met UserNotFound tot gevolg. + """ + app = WORTEL / "apps" / "unifi-network" + doc = _render(app, tmp_path) + reparatie = doc["services"]["unifi-mongo-init"] + assert any(v.endswith(":/unifi-config") for v in reparatie["volumes"]) + + # De reparatie draaien tegen een bestaand bestand van vóór de authSource. + config = tmp_path / "unifi-config" / "data" + config.mkdir(parents=True) + props = config / "system.properties" + props.write_text( + "unifi.https.port=8443\n" + "db.mongo.local=false\n" + "db.mongo.uri=mongodb://unifi:oudgeheim@unifi-mongo:27017/unifi?tls=false\n" + "statdb.mongo.uri=mongodb://unifi:oudgeheim@unifi-mongo:27017/unifi_stat?tls=false\n" + "unifi.db.name=unifi\n", encoding="utf-8") + + nep = tmp_path / "nepbin" + nep.mkdir() + (nep / "mongo").write_text("#!/bin/bash\ncat > /dev/null\n", encoding="utf-8") + (nep / "mongo").chmod(0o755) + omgeving = { + "PATH": f"{nep}:/usr/bin:/bin", + "MONGO_INITDB_ROOT_USERNAME": "unifi", "MONGO_INITDB_ROOT_PASSWORD": "nieuw@geheim", + "MONGO_USER": "unifi", "MONGO_PASS": "nieuw@geheim", + "MONGO_HOST": "unifi-mongo", "MONGO_PORT": "27017", + "MONGO_DBNAME": "unifi", "MONGO_AUTHSOURCE": "admin", + "UNIFI_CONFIG_DIR": str(tmp_path / "unifi-config"), + } + r = subprocess.run(["bash", str(tmp_path / "init-mongo.sh")], + capture_output=True, text=True, env=omgeving, timeout=30) + assert r.returncode == 0, r.stdout + r.stderr + + regels = dict(l.split("=", 1) for l in props.read_text().strip().splitlines()) + assert "authSource=admin" in regels["db.mongo.uri"] + assert "authSource=admin" in regels["statdb.mongo.uri"] + # Bijzondere tekens horen ge-url-codeerd te zijn, anders knipt de @ de URI + # doormidden. + assert "nieuw%40geheim" in regels["db.mongo.uri"] + assert "oudgeheim" not in props.read_text() + assert regels["statdb.mongo.uri"].split("?")[0].endswith("/unifi_stat") + # Instellingen die niet over de database gaan blijven staan. + assert regels["unifi.https.port"] == "8443" + + +def test_unifi_probeert_ook_het_wachtwoord_uit_system_properties(tmp_path): + """Wijkt het wachtwoord in .env af van dat waarmee de database ooit is + aangemaakt, dan is het wachtwoord in system.properties het enige waarmee we + nog binnenkomen — en dus de enige weg om alles weer gelijk te trekken.""" + _render(WORTEL / "apps" / "unifi-network", tmp_path) + config = tmp_path / "unifi-config" / "data" + config.mkdir(parents=True) + (config / "system.properties").write_text( + "db.mongo.uri=mongodb://unifi:oud%40geheim@unifi-mongo:27017/unifi?tls=false\n", + encoding="utf-8") + + nep = tmp_path / "nepbin" + nep.mkdir() + # De 'mongo-shell' schrijft op wat hij binnenkrijgt, zodat we kunnen zien + # welke wachtwoorden geprobeerd worden. + (nep / "mongo").write_text( + f"#!/bin/bash\ncat > {tmp_path / 'ontvangen.js'}\n", encoding="utf-8") + (nep / "mongo").chmod(0o755) + r = subprocess.run( + ["bash", str(tmp_path / "init-mongo.sh")], capture_output=True, text=True, + env={"PATH": f"{nep}:/usr/bin:/bin", "MONGO_INITDB_ROOT_USERNAME": "unifi", + "MONGO_INITDB_ROOT_PASSWORD": "nieuwgeheim", "MONGO_USER": "unifi", + "MONGO_PASS": "nieuwgeheim", "MONGO_HOST": "unifi-mongo", + "MONGO_PORT": "27017", "MONGO_DBNAME": "unifi", + "MONGO_AUTHSOURCE": "admin", + "UNIFI_CONFIG_DIR": str(tmp_path / "unifi-config")}, + timeout=30) + assert r.returncode == 0, r.stdout + r.stderr + + js = (tmp_path / "ontvangen.js").read_text() + # %40 hoort weer een @ te zijn voordat MongoDB hem te zien krijgt. + assert 'const oudWachtwoord = "oud@geheim";' in js + assert 'const beheerWachtwoord = "nieuwgeheim";' in js + + +def test_unifi_reparatie_werkt_ook_zonder_bestaande_system_properties(tmp_path): + """Bij een verse installatie schrijft het image het bestand zelf; de + reparatie mag daar niet over struikelen.""" + _render(WORTEL / "apps" / "unifi-network", tmp_path) + nep = tmp_path / "nepbin" + nep.mkdir() + (nep / "mongo").write_text("#!/bin/bash\ncat > /dev/null\n", encoding="utf-8") + (nep / "mongo").chmod(0o755) + r = subprocess.run( + ["bash", str(tmp_path / "init-mongo.sh")], capture_output=True, text=True, + env={"PATH": f"{nep}:/usr/bin:/bin", "MONGO_INITDB_ROOT_USERNAME": "unifi", + "MONGO_INITDB_ROOT_PASSWORD": "x", "MONGO_USER": "unifi", "MONGO_PASS": "x", + "MONGO_HOST": "unifi-mongo", "MONGO_DBNAME": "unifi", + "MONGO_AUTHSOURCE": "admin", + "UNIFI_CONFIG_DIR": str(tmp_path / "bestaat-niet")}, + timeout=30) + assert r.returncode == 0, r.stdout + r.stderr + AMD64_ALLEEN = { "cal-com", "comfyui", "dawarich", "focalboard", "formbricks", "mattermost", "pinchflat", "pixelfed", "tubearchivist", "unbound", "wazuh",