fix(linux): resolve MySQL root password removal and prompt for current password when set - #6
Conversation
5dea510 to
7ff6702
Compare
wpexpertinbd
left a comment
There was a problem hiding this comment.
Appreciate you tackling the root-password flow — and thanks for the clean commit history; the superset structure over #5 made this easy to review. Supply-chain pass is clean (no new network calls, downloads, eval/base64, or dependencies — the only new exec surface is pkexec/sudo running the locally installed mysql client, which is legitimate here), and your mysql_esc handling is applied correctly to all three SQL variants, so there's no injection path through the password. The Python side also correctly passes secrets via subprocess env= rather than argv, and correctly keeps db root-passwd out of _PRIVILEGED so the env survives — you clearly read the existing design.
All three blockers from my review on #5 apply here too (Swift syntax → macOS won't compile; the name NameError killing "Add app"; Laravel's public/ never created). Four more on the DB work:
1. Please don't persist the root password. db_root_passwd calls json_set "root_password" "$DB_PASSWORD" on all five success paths, which writes it in plaintext to ~/.bhserve/config/bhserve.json. There's no chmod/umask anywhere in the engine, so that file is 0644 in a 0755 dir — world-readable on a shared machine. Worse, cmd_api splices the file verbatim into the GUI snapshot, which the app polls every 4 seconds, and status / config show print it too. That also reverses two explicit decisions already commented in the code (engine/bhserve: "env keeps it out of ps", and engine.py: "owner-only /proc//environ instead of argv").
Your new "Current password" prompt makes prompting the better answer. If it really must be stored, it needs: a dedicated file created under umask 077 with the mode re-asserted on every rewrite (cat > preserves the old mode), chmod 600 in _bh_fix_ownership, redaction in cmd_api/cmd_status, and root_password removed from config_set.
2. Keep the SQL out of argv. pkexec "$cli" -e "$sql" / $SUDO "$cli" -e "$sql" put ALTER USER ... IDENTIFIED BY 'pw' into the process table — the mysql client scrubs -p<pw> but not -e, and both pkexec and sudo record the full command vector in the audit log permanently. stdin survives pkexec, so this works and fixes it:
printf '%s\n' "$sql" | ${SUDO:-sudo} "$cli"That ${SUDO:-sudo} is needed anyway: $SUDO is only assigned in platform-linux.sh, but you use it in the shared engine/bhserve, which runs set -u — on macOS (no pkexec) that branch always runs and aborts with SUDO: unbound variable before your legacy-SQL fallbacks.
3. nginx_reload → nginx_restart is the one I'd most like reverted. You're right that the old [ -t 1 ] tty gate meant GUI-initiated changes were never applied — please keep that fix. But -s reload is fail-safe: it keeps serving the old config if a vhost is bad. nginx_restart stops first and then dies in nginx -t, and because the call is wrapped in >/dev/null 2>&1 the message is swallowed and the || -s reload fallback is unreachable — so one bad vhost takes every site down, silently. We shipped an emergency release for exactly that failure mode a few days ago (c8765f3). Suggested shape:
nginx -s reload ... || { nginx_stop; nginx_start; } # reload primary, restart as fallback(and the mirror in Nginx.Reload() on Windows, which currently discards Start()'s (bool, string) result).
4. The stored password can't round-trip. jget's capture is [^",}]*, so any password containing ,, " or } reads back truncated (abc,def → abc), and the unescaped JSON write can produce an invalid config — which, because cmd_api inlines the file, blanks the entire dashboard while still printing root password set. json_esc() already exists in the same file and is used ~15×. This mostly disappears if you drop the persistence per (1).
One request: would you be open to splitting this? The folder pickers, the Laravel type, the nginx reload change, and the DB root-password work are four independent features — smaller PRs would let me merge the good parts fast instead of holding them behind the DB discussion. Thanks again for the solid work here.
…no longer break every site)
A site whose document root contains a space rendered as a bare `root /srv/My Site;`,
which nginx rejects with "invalid number of arguments in "root" directive" — a fatal
config error that stops nginx for EVERY site, the same outage class fixed in 1.0.68.
The readers made it worse: `awk '{print $2}'` silently truncated such a root to
`/srv/My`, which was then re-rendered on the next site php/secure — and one call site
feeds that value to rm -rf under `site rm --purge`.
Pre-existing bug, but a folder picker (community PR #5/#6) makes spaced paths likely,
so fix it at the source:
- Writers now quote: `root "$root";` in all three nginx templates (php vhost,
apache-front, OLS-front) and both Windows C# templates. Apache's DocumentRoot was
already quoted — that was the model.
- New vhost_root_read() (bash) / VhostRoot() (C#) replaces all 11 bash and 2 C#
readers. Accepts both the new quoted form and the legacy unquoted form still on
disk in every existing install, plus an optional trailing comment.
- New valid_site_root() / ValidateSiteRoot() on site add and site root reject ; " { }
$ \ and newlines — the characters that inject into or break the vhost. Spaces are
explicitly allowed; that is the point of the quoting.
Verified in WSL (17/17): a spaced root renders quoted, nginx stays valid and the site
serves, a re-render round-trips the full path, the api reports it intact, legacy
unquoted vhosts still parse, and every injection payload is refused with nginx
untouched. C# compiles clean. macOS inherits via the shared engine.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the missing build CI. BHServe ships three front-ends (Swift/macOS, C#/Windows, Python/Linux) over one shared bash engine and nothing ever compiled the other platforms. - macOS: swift build - Windows: dotnet build BHServe.sln - Linux: compileall + ruff F821/F822 (undefined names) + bash -n over the engine and every shipped .sh Verified by injecting the two real bugs found in community PR #5/#6 and confirming the gate goes red with precise file:line messages (AppState.swift:618 'expected type after is'; window.py:441 'Undefined name name'), then green again on revert. Windows stayed green throughout, confirming per-platform isolation.
|
Status update after the review — here's where things stand on this branch: Fixed here:
Still open here:
Happy to port the shared fixes over and address the DB items — or, per your suggestion, split the DB work out so the folder pickers / Laravel type / nginx reload can land independently. Which do you prefer? |
|
#5 is merged — thanks again. To your question: let's split, and this branch becomes DB-only. Concretely, if you rebase on
So no need to port anything here. What's left for #6 is purely the MySQL root-password work, and the four items from my review:
One heads-up: No rush, and thanks for being so responsive to the review. |
…t password when set
…t of argv
- db_root_passwd no longer persists root_password to bhserve.json (all 5 paths
dropped); the GUI re-prompts for the current password instead, so nothing is
stored and nothing needs json_esc/round-trip handling.
- Remove the now-dead saved_pw config fallback from mysql_run (both engines).
- pkexec/sudo fallbacks pipe the SQL on stdin (printf '%s\n' "" | ${SUDO-sudo}
"$cli") instead of -e "$sql", keeping the ALTER out of ps + the polkit/sudo
audit log; ${SUDO-sudo} also fixes the set -u unbound-variable abort on macOS
while honoring the Linux SUDO="" = already-root convention.
- config show/cmd_api/cmd_status now redact any legacy root_password (config_dump)
and fix the trailing comma a pre-fix build left, so the 4s GUI poll stays clean.
af77141 to
0159c44
Compare
wpexpertinbd
left a comment
There was a problem hiding this comment.
Rebase looks great — this is DB-only now (3 files, +132/-22) and three of the four items are fully verified fixed. One blocker left, and it's a behaviour I can reproduce deterministically.
🔴 Blocker: the pkexec fallback hangs forever where no polkit agent is running
if printf '%s\n' "$sql" | pkexec "$cli" 2>/dev/null; thenThe branch is gated on [ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ], but a set DISPLAY doesn't mean an authentication agent is there to answer. With no agent, pkexec blocks indefinitely — I left it stuck for ~10 minutes on pkexec /usr/bin/mariadb before killing it.
A/B against master, identical conditions (MariaDB stopped, so the unprivileged connect fails and we reach the fallback):
| result | |
|---|---|
master |
exit 1 in 0s — ✗ failed (server running? do you have privilege?) |
| this branch | hung, killed at 25s (exit 124) |
This matters because db root-passwd is invoked from the GUI, so the app locks up with no way to cancel — the same "BHServe Is Not Responding" failure we shipped a fix for in 1.0.50. And it's reachable in ordinary use, not just an edge case: any time the unprivileged connect fails first — the DB isn't running, or the user mistypes the current password. On a full GNOME desktop it won't hang, but it pops a system root auth dialog to set a MySQL password, which is surprising for a local-dev tool.
Suggested fix — just drop Fallback 1 (the pkexec/sudo block). It's redundant on Linux: mysql_run in platform-linux.sh already ends with the two $SUDO branches you added, and those are safe because the GUI runs $SUDO as sudo -n (non-interactive, fails fast — 1.0.50). So the privileged attempt already happens, without the hang. Keeping master's fail-fast die gives the user an actionable message instead of a frozen window. Fallback 2 (legacy SQL syntax) is fine as-is — it goes through mysql_run, so no escalation.
If you'd rather keep a privileged attempt, it needs to be non-blocking (e.g. timeout 20 pkexec …) and only run when an agent is actually present — but honestly the fail-fast path reads better here.
✅ Verified working (tested, not just read)
1. Password no longer persisted — json_set "root_password" is gone; root_password now appears only in the redaction. Nice touch adding config_dump() to scrub a legacy key written by the earlier revision of this branch, and wiring it into cmd_api / cmd_status / config show — that's more than I asked for. I tested your sed against all three shapes, since a malformed config would blank the dashboard on the 4s poll:
- no legacy key → valid JSON, all keys intact
root_passwordin the middle → removed, valid JSONroot_passwordas the last key (the trailing-comma trap) →"autostart": false}— valid JSON, secret gone
bhserve api stayed valid JSON with sites/services intact and leaked nothing; status redacts too.
2. SQL out of argv — printf '%s\n' "$sql" | … ✅. And you were right to use ${SUDO-sudo} rather than the ${SUDO:-sudo} I suggested: the single - preserves an explicitly-empty SUDO (already root) instead of re-escalating. Good catch, and thanks for documenting why.
3. json_esc/jget round-trip — moot now that nothing is persisted. Correct call: removing the feature removed the bug class.
4. Works with no password set — full round-trip verified: blank → set → clear using the current password → blank, with db list still working throughout. The dialog correctly only shows "Current" when one is set.
All three CI checks are green.
Minor, optional (pre-existing — not caused by this PR)
The password entries aren't masked (Gtk.Entry without set_visibility(False)). Our existing _pw_dialog has the same gap, so this isn't a regression — but since you're already in this dialog, adding set_visibility(False) to both entries (with a reveal toggle if you like) would be a nice win.
Fix the pkexec branch and I'll merge — everything else here is solid work.
Summary
window.pynow prompts for the current password as well as the new password (leave blank to remove).mysql_runinengine/bhserveandplatform-linux.shauthenticates usingBHSERVE_OLD_DB_PASSWORD/MYSQL_PWDor saved credentials.pkexec/sudoelevation on Linux if unprivileged connection fails.ALTER USER 'root'@'localhost' IDENTIFIED BY ''syntax for MySQL 8.0/8.4 compatibility.