Files
rsync/testsuite/daemon-auth_test.py
Andrew Tridgell 05f30c05c9 testsuite: daemon parameter coverage (loopback)
Drive a loopback daemon (secure stdio-pipe transport by default, also green
under --use-tcp) via the new write_daemon_conf helper and assert the behaviour
of the security-relevant rsyncd.conf parameters, transferring >=3-deep trees:

  daemon-access  path / read only / write only / list, incl. a deep sub-path
                 pull and that a list=no module is hidden yet usable by name.
  daemon-filter  daemon exclude hides matching files everywhere; incoming /
                 outgoing chmod rewrite modes of every transferred file.
  daemon-auth    auth users + secrets file accept the right password, reject a
                 wrong one and an unauthenticated request; strict modes rejects
                 a world-readable secrets file.
  daemon-exec    pre-/post-xfer exec run with RSYNC_MODULE_NAME /
                 RSYNC_EXIT_STATUS; a failing pre-xfer exec aborts the transfer
                 (marker files polled for, since post-xfer exec runs after the
                 client disconnects under TCP).
  daemon-munge   munge symlinks stores incoming links with the /rsyncd-munged/
                 prefix and strips it on the way out.
  daemon-refuse  refuse options: a named option, a wildcard, and the '* !a !v'
                 allow-list idiom.

Green on master under pipe and --use-tcp transports and under --protocol=29.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:31:52 +10:00

93 lines
3.1 KiB
Python

#!/usr/bin/env python3
"""Daemon coverage: auth users, secrets file, strict modes.
A module with auth users + a secrets file must accept the right password,
reject a wrong one, and (with the default strict modes) refuse a
world-readable secrets file. Authentication happens in the daemon protocol, so
it works over the default secure stdio-pipe transport.
"""
import os
import subprocess
from rsyncfns import (
FROMDIR, SCRATCHDIR,
make_tree, makepath, rmtree, rsync_argv, start_test_daemon, test_fail,
verify_dirs, write_daemon_conf,
)
DAEMON_PORT = 12888
# When a daemon module needs auth and no password is available, rsync falls back
# to an interactive getpass() prompt that reads /dev/tty directly -- which the
# test harness cannot redirect, so it would hang `make coverage` (or any run
# with a controlling terminal). Give every client a fallback password via the
# environment so it never prompts: the --password-file cases below override it,
# and the invalid-credentials case uses it and is correctly rejected.
os.environ['RSYNC_PASSWORD'] = 'env-fallback-wrong'
src = FROMDIR
rmtree(src)
make_tree(src, depth=3)
authdir = SCRATCHDIR / 'authdest'
secrets = SCRATCHDIR / 'rsyncd.secrets'
secrets.write_text('tuser:secretpass\n')
secrets.chmod(0o600)
conf = write_daemon_conf([
('auth', {'path': authdir, 'read only': 'no',
'auth users': 'tuser', 'secrets file': secrets}),
])
url = start_test_daemon(conf, DAEMON_PORT)
userurl = url.replace('rsync://', 'rsync://tuser@', 1)
def pwfile(name, text):
p = SCRATCHDIR / name
p.write_text(text)
p.chmod(0o600)
return p
def push(pw, **kw):
rmtree(authdir)
makepath(authdir)
return subprocess.run(
rsync_argv('-a', f'--password-file={pw}', f'{src}/', f'{userurl}auth/'),
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True, **kw)
# --- correct password succeeds ----------------------------------------------
ok = pwfile('pw.ok', 'secretpass\n')
proc = push(ok)
if proc.returncode not in (0, 23):
test_fail(f"auth with the correct password failed: {proc.stderr}")
verify_dirs(src, authdir, label="auth success")
# --- wrong password is rejected ---------------------------------------------
bad = pwfile('pw.bad', 'wrongpass\n')
proc = push(bad)
if proc.returncode == 0:
test_fail("auth with the wrong password unexpectedly succeeded")
# --- a request with invalid credentials is rejected ------------------------
# Local user (not an auth user) with the wrong env-supplied password; rejected
# without ever prompting on the tty.
proc = subprocess.run(
rsync_argv('-a', f'{src}/', f'{url}auth/'),
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True,
stdin=subprocess.DEVNULL)
if proc.returncode == 0:
test_fail("a request with invalid credentials succeeded against an "
"auth-users module")
# --- strict modes rejects a world-readable secrets file ---------------------
secrets.chmod(0o644)
proc = push(ok)
if proc.returncode == 0:
test_fail("strict modes did not reject a world-readable secrets file")
secrets.chmod(0o600)
print("daemon-auth: auth users / secrets file / strict modes verified")