fix(unifi): databasegegevens bij elke start gelijktrekken
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 5m46s

Het linuxserver-image schrijft /config/data/system.properties alleen bij de
allereerste start. De authSource en het wachtwoord die daarna in Compose
veranderden kwamen er nooit in, dus UniFi bleef verbinden met de gegevens van
de eerste installatie — MongoDB antwoordde met UserNotFound: unifi@unifi.

- De reparatiecontainer zet db.mongo.uri, statdb.mongo.uri, db.mongo.local en
  unifi.db.name opnieuw uit de actuele omgeving; overige instellingen blijven.
- Gebruikersnaam en wachtwoord worden url-gecodeerd in de URI gezet.
- Het bestand wordt op zijn plaats overschreven, dus eigenaar en rechten
  blijven van UniFi zelf.
- Het wachtwoord uit system.properties telt mee als inlogpoging, zodat een
  afgeweken .env de stack niet onherstelbaar maakt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9Vcwu7owJZ3nj5dCf4VFT
This commit is contained in:
Ramon 2026-08-06 20:24:54 +02:00
parent 3ee09eebc7
commit dce4e34894
5 changed files with 210 additions and 6 deletions

View file

@ -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

View file

@ -1 +1 @@
0.8.21-beta
0.8.22-beta

View file

@ -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:

View file

@ -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[@]}" <<EOF
const oudWachtwoord = "${oud_wachtwoord}";
const beheerNaam = "${MONGO_INITDB_ROOT_USERNAME}";
const beheerWachtwoord = "${MONGO_INITDB_ROOT_PASSWORD}";
const appNaam = "${MONGO_USER}";
@ -33,15 +57,22 @@ const adminDb = db.getSiblingDB("admin");
// mislukt de reparatie juist op de installaties die haar nodig hebben.
const kandidaten = [beheerNaam, appNaam, "root", "unifi"]
.filter((naam, i, alle) => 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) {
for (const wachtwoord of wachtwoorden) {
try {
if (adminDb.auth(naam, beheerWachtwoord)) {
if (adminDb.auth(naam, wachtwoord)) {
return naam;
}
} catch (e) {
// Onbekende gebruiker of verkeerd wachtwoord: volgende kandidaat.
// Onbekende gebruiker of verkeerd wachtwoord: volgende combinatie.
}
}
}
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 %>

View file

@ -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",