synology: fix SSH backend syno* CLI resolution (full pre-test verification)

Found during a full command-surface recheck: every privileged SSH recipe
(shares/users/groups/acl) was broken — sudo secure_path drops /usr/syno/{bin,sbin}
so synoshare/synouser/synogroup/synoacltool were "command not found" (non-sudo
plain recipes worked because the admin login PATH has them).

- Inject SYNO_PATH into priv()/plain(); run priv via `sh -c` so operators work.
- synouser/synogroup use `--enum local` (not the invalid `--list`).
- acl quotes the share path (handles spaces, e.g. "Sandra Fish").
- services repointed to Web API (no synoservice on DSM 7.2; synosystemctl has no list-all).

Verified live: all Web API reads, all SSH reads (acl returns real Windows ACEs),
write path (share create/delete), and every destructive command correctly gated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-25 13:01:03 -07:00
parent a7ceb5f793
commit 5b3dd84fb9
2 changed files with 24 additions and 7 deletions

View File

@@ -140,6 +140,16 @@ VPN-down connect error surfaced to the user, a method refused for lack of --conf
hint on the FileStation 400/407 denial. **`--confirm`/`--vault` after the subcommand were rejected
by argparse** (every documented gated-write example, e.g. `call X set k=v --confirm`, would have
failed) — moved to a shared parent parser so both flags now work before AND after the subcommand.
- **SSH backend fully verified + fixed (2026-06-25):** the `priv` (sudo) recipes — `shares`,
`users`, `groups`, `acl` — were ALL broken: `sudo`'s `secure_path` drops `/usr/syno/{bin,sbin}`,
so `synoshare`/`synouser`/`synogroup`/`synoacltool` returned "command not found" (the non-sudo
`plain` recipes worked because the admin login PATH has those dirs). Fixed by injecting
`SYNO_PATH` and running `priv` via `sh -c` (so shell operators survive). Also: `synouser`/
`synogroup` use `--enum local` (not `--list`); `acl` quotes the share path (handles "Sandra
Fish"); `services` repointed to the Web API (`synoservice` doesn't exist on DSM 7.2, and
`synosystemctl` has no list-all). Verified live: `info` `df` `shares` `users`(41) `groups`(4)
`packages` `acl Server`(real Windows ACEs) `acl Public`(Linux-mode) all OK. `acl` on a
Windows-ACL share is the SSH backend's unique value (the per-file ACE list the Web API can't give).
- **Code-review hardening (2026-06-25, /code-review high):** `SynoError` now carries the DSM `code`
+ a `handled` flag; `call()` no longer logs eagerly — the top-level handler logs only genuine
unhandled failures, so the handled FileStation denial (and VPN-down connect errors) no longer

View File

@@ -46,21 +46,28 @@ else
run_ssh() { local rc; SYNO_SSH_PW="$P" SSH_ASKPASS="$ASKPASS" SSH_ASKPASS_REQUIRE=force DISPLAY="${DISPLAY:-:0}" ssh "${SSH_OPTS[@]}" "$@" || { rc=$?; [ "$rc" = 255 ] && logerr "syno SSH connect/auth failed (rc=255)" "host=$H vp=$VP"; return $rc; }; }
fi
# privileged remote command: feed the admin password to `sudo -S`
priv() { run_ssh "$U@$H" "echo '$P' | sudo -S -p '' $1" 2>&1 | grep -v '^Password:' ; }
plain() { run_ssh "$U@$H" "$1" 2>&1 | grep -viE 'Permanently added'; }
# The syno* CLI lives in /usr/syno/{bin,sbin}. The admin's interactive login PATH has
# these (so `plain` worked), but `sudo`'s secure_path drops them -> `priv` got
# "command not found" for synoshare/synouser/synoacltool/... Inject the dirs explicitly.
SYNO_PATH='/usr/syno/bin:/usr/syno/sbin:/usr/local/bin:/bin:/usr/bin:/sbin:/usr/sbin'
# privileged remote command: feed the admin password to `sudo -S`, with the syno bin dirs on
# PATH. Run via `sh -c` so shell operators (|, ||, ;) in the recipe all execute as root+PATH.
priv() { run_ssh "$U@$H" "echo '$P' | sudo -S -p '' env PATH='$SYNO_PATH' sh -c '$1'" 2>&1 | grep -v '^Password:' ; }
plain() { run_ssh "$U@$H" "export PATH='$SYNO_PATH':\$PATH; $1" 2>&1 | grep -viE 'Permanently added'; }
case "$RECIPE" in
info) plain 'uname -a; echo; cat /etc/synoinfo.conf 2>/dev/null | grep -iE "^(productversion|buildnumber|unique|upnpmodelname)" ; echo; cat /proc/meminfo | head -1';;
df) plain 'df -h | grep -E "Filesystem|/volume"';;
shares) priv 'synoshare --enum ALL';;
users) priv 'synouser --list local || synouser --enum local';;
groups) priv 'synogroup --list || synogroup --enum local';;
users) priv 'synouser --enum local';;
groups) priv 'synogroup --enum local';;
packages) plain 'synopkg list 2>/dev/null || ls /var/packages';;
services) priv 'synoservice --list 2>/dev/null | head -80';;
services) # DSM 7.2 has no `synoservice`/synosystemctl list-all -> service enumeration is Web-API only.
echo "[INFO] service enumeration is Web-API only on DSM 7.2 (no synoservice CLI)."
echo " use: bash .claude/scripts/py.sh .claude/skills/synology/scripts/syno_client.py services";;
acl)
SHARE="${POS[1]:?acl needs a share name, e.g. acl Server}"
priv "synoacltool -get /volume1/$SHARE";;
priv "synoacltool -get \"/volume1/$SHARE\"";;
reboot)
[ "$CONFIRM" = "1" ] || { echo "[BLOCKED] reboot the NAS — re-run with --confirm"; exit 2; }
echo "[INFO] rebooting $H via synoshutdown -r (Web-API 103 fallback)"; priv 'synoshutdown -r';;