diff --git a/src/serverfs/usr/sbin/linbo-torrent b/src/serverfs/usr/sbin/linbo-torrent index e388923..925a0b9 100755 --- a/src/serverfs/usr/sbin/linbo-torrent +++ b/src/serverfs/usr/sbin/linbo-torrent @@ -52,11 +52,18 @@ usage(){ } # update image permissions +# *.macct files hold samba machine account password hashes. The [linbo] +# rsync module serves LINBODIR as nobody without login, so they must never +# belong to nobody, at any depth (backups, .incoming, ...). They are left out +# of the nobody chown rather than taken back afterwards. +# images/.incoming is the staging dir of the central image sync +# (linuxmuster-tools), which keeps it root 700 while an upload waits there, +# so it is left out of the nobody chown too. update_permissions(){ - chown nobody:nogroup "$LINBOIMGDIR" -R + find "$LINBOIMGDIR" -type f -name "*.macct" -exec chown root:root {} + -exec chmod 600 {} + + find "$LINBOIMGDIR" -path "$LINBOIMGDIR/.incoming" -prune \ + -o ! \( -type f -name "*.macct" \) -exec chown -h nobody:nogroup {} + chown "$OTRUSER":"$OTRUSER" "$OTRDIR" -R - chown root:root "$LINBOIMGDIR"/*/*.macct - chmod 600 "$LINBOIMGDIR"/*/*.macct } # save torrent hash diff --git a/src/serverfs/usr/share/linuxmuster/linbo/rsync-post-upload.sh b/src/serverfs/usr/share/linuxmuster/linbo/rsync-post-upload.sh index 736c36f..64dc6e5 100755 --- a/src/serverfs/usr/share/linuxmuster/linbo/rsync-post-upload.sh +++ b/src/serverfs/usr/share/linuxmuster/linbo/rsync-post-upload.sh @@ -118,8 +118,12 @@ case "$EXT" in if [ -n "$unicodepwd" ]; then echo "Saving samba password hash and kvno for $image." template="$LINBOTPLDIR/machineacct" - sed -e "s|@@unicodepwd@@|$unicodepwd|" \ - -e "s|@@suppcredentials@@|$suppcredentials|" "$template" > "$imagemacct" + # create it anew with mode 600, the [linbo] rsync module can read it + # as nobody while it is still 644, and writing into an existing file + # keeps its mode until the chmod + rm -f "$imagemacct" + (umask 077; sed -e "s|@@unicodepwd@@|$unicodepwd|" \ + -e "s|@@suppcredentials@@|$suppcredentials|" "$template" > "$imagemacct") chmod 600 "$imagemacct" # remove obsolete macct file if present rm -f "$IMGDIR/${BASE}.macct" diff --git a/tests/shell/test_linbo-torrent.sh b/tests/shell/test_linbo-torrent.sh new file mode 100644 index 0000000..5b0f869 --- /dev/null +++ b/tests/shell/test_linbo-torrent.sh @@ -0,0 +1,158 @@ +#!/bin/sh +# +# test_linbo-torrent.sh +# tom.lehmann@netzint.de +# 20260929 +# +# update_permissions: machine account files (*.macct) anywhere below the +# images dir end up root:root 600 and are never handed to nobody on the way, +# everything else keeps going to nobody:nogroup (opentracker dir to its +# user) except the image sync's staging dir images/.incoming, symlinks are +# not followed, and no errors without any *.macct. +# +# linbo-torrent is a bash script on the server, so the function runs under +# bash here, whichever shell runs the tests. chown is replaced by a stub on +# PATH that logs "owner path" per file instead of changing anything, since +# the tests don't run as root (busybox ash would ignore the stub and run its +# own chown applet). chmod is the real one. + +SCRIPT="$(dirname "$0")/../../src/serverfs/usr/sbin/linbo-torrent" + +oneTimeSetUp() { + . "$(dirname "$0")/lib/extract_function.sh" + extract_function "$SCRIPT" update_permissions > "$SHUNIT_TMPDIR/linbo_torrent_update_permissions.sh" + mkdir -p "$SHUNIT_TMPDIR/bin" + cat > "$SHUNIT_TMPDIR/bin/chown" <<'EOF' +#!/bin/sh +# chown stub: logs "owner path" for every file chown would change. -R logs +# the whole tree without following symlinks; a symlink given without -h +# logs its target, as chown would change that one. +recursive="" +nodereference="" +for arg; do + case "$arg" in + -R) recursive=1 ;; + -h) nodereference=1 ;; + esac +done +owner="" +rc=0 +for arg; do + case "$arg" in -*) continue ;; esac + if [ -z "$owner" ]; then + owner="$arg" + elif [ ! -e "$arg" ] && [ ! -L "$arg" ]; then + echo "chown: cannot access '$arg': No such file or directory" >&2 + rc=1 + elif [ -n "$recursive" ]; then + find "$arg" | sed "s|^|$owner |" >> "$CHOWN_LOG" + elif [ -L "$arg" ] && [ -z "$nodereference" ]; then + echo "$owner $(readlink -f "$arg")" >> "$CHOWN_LOG" + else + echo "$owner $arg" >> "$CHOWN_LOG" + fi +done +exit "$rc" +EOF + chmod 755 "$SHUNIT_TMPDIR/bin/chown" +} + +setUp() { + images="$SHUNIT_TMPDIR/images" + outside="$SHUNIT_TMPDIR/outside" + CHOWN_LOG="$SHUNIT_TMPDIR/chown.log" + STDERR_LOG="$SHUNIT_TMPDIR/stderr.log" + rm -rf "$images" "$outside" "$CHOWN_LOG" "$STDERR_LOG" + mkdir -p "$images/win10/backups/202609011200" "$images/tmp" \ + "$images/.incoming/win11" "$images/opentracker" "$outside" + for f in win10/win10.qcow2 win10/win10.qcow2.info win10/win10.qcow2.macct \ + win10/backups/202609011200/win10.qcow2 \ + win10/backups/202609011200/win10.qcow2.macct \ + tmp/win10.qcow2.macct \ + .incoming/win11/win11.qcow2 .incoming/win11/win11.qcow2.macct \ + opentracker/torrent.whitelist; do + echo "$f" > "$images/$f" + chmod 644 "$images/$f" + done + echo secret > "$outside/secret" + chmod 644 "$outside/secret" +} + +run_update_permissions() { + CHOWN_LOG="$CHOWN_LOG" PATH="$SHUNIT_TMPDIR/bin:$PATH" \ + LINBOIMGDIR="$images" OTRDIR="$images/opentracker" OTRUSER="_opentracker" \ + bash -c '. "$1" && update_permissions' sh \ + "$SHUNIT_TMPDIR/linbo_torrent_update_permissions.sh" 2> "$STDERR_LOG" +} + +# owner a file got last +owner_of() { + awk -v f="$1" '$2 == f { owner = $1 } END { print owner }' "$CHOWN_LOG" +} + +macct_files() { + echo "$images/win10/win10.qcow2.macct" + echo "$images/win10/backups/202609011200/win10.qcow2.macct" + echo "$images/tmp/win10.qcow2.macct" + echo "$images/.incoming/win11/win11.qcow2.macct" +} + +test_macct_files_end_up_root_600_at_any_depth() { + run_update_permissions + for f in $(macct_files); do + assertEquals "owner of $f" "root:root" "$(owner_of "$f")" + assertEquals "mode of $f" "600" "$(stat -c %a "$f")" + done +} + +test_macct_files_are_never_given_to_nobody() { + run_update_permissions + for f in $(macct_files); do + assertFalse "$f given to nobody" "grep -qxF 'nobody:nogroup $f' '$CHOWN_LOG'" + done +} + +test_other_files_go_to_nobody_unchanged_mode() { + run_update_permissions + for f in "$images" "$images/win10" "$images/win10/win10.qcow2" \ + "$images/win10/win10.qcow2.info" "$images/win10/backups" \ + "$images/win10/backups/202609011200" \ + "$images/win10/backups/202609011200/win10.qcow2" "$images/tmp"; do + assertEquals "owner of $f" "nobody:nogroup" "$(owner_of "$f")" + done + assertEquals "644" "$(stat -c %a "$images/win10/win10.qcow2")" +} + +test_incoming_is_left_to_the_image_sync() { + run_update_permissions + assertFalse ".incoming given to nobody" \ + "grep -qF 'nobody:nogroup $images/.incoming' '$CHOWN_LOG'" + assertEquals "root:root" \ + "$(owner_of "$images/.incoming/win11/win11.qcow2.macct")" +} + +test_opentracker_dir_goes_to_opentracker_user() { + run_update_permissions + assertEquals "_opentracker:_opentracker" "$(owner_of "$images/opentracker")" + assertEquals "_opentracker:_opentracker" \ + "$(owner_of "$images/opentracker/torrent.whitelist")" +} + +test_no_errors_without_macct_files() { + for f in $(macct_files); do + rm -f "$f" + done + run_update_permissions + assertEquals 0 $? + assertEquals "" "$(cat "$STDERR_LOG")" +} + +test_symlinks_are_not_followed() { + ln -s "$outside/secret" "$images/win10/link.qcow2.macct" + ln -s "$outside/secret" "$images/win10/link.qcow2" + run_update_permissions + assertFalse "symlink target chowned" "grep -qF '$outside' '$CHOWN_LOG'" + assertEquals "644" "$(stat -c %a "$outside/secret")" +} + +. "$(dirname "$0")/shunit2"