diff options
author | Martin Pitt <martinpitt@gnome.org> | 2013-01-08 11:08:02 +0100 |
---|---|---|
committer | Martin Pitt <martinpitt@gnome.org> | 2013-02-12 05:48:34 +0100 |
commit | 7b331d0c8331f577311a3d94a4651e8fdfa7a19f (patch) | |
tree | 4dc6cc6661294c533f561d779019595c8223192b /test | |
parent | 26d731dccfa4732b8b4b94027cbd65929e1e1a03 (diff) | |
download | gvfs-7b331d0c8331f577311a3d94a4651e8fdfa7a19f.tar.gz |
Add gvfs-testbed to enable tests which need root
The Drive test requires root privileges as it uses the scsi_debug kernel module
and running some commands as root, such as injecting a temporary udev rule for
working around some scsi_debug limitations and running udisksd under a mock
polkit daemon.
Add a "gvfs-testbed" script which sets up some unshared tmpdir overlays as a
sandbox (to ensure that the tests don't destroy anything in the real system),
set up a temporary user etc.
This also enables the Sftp.test_unknown_host, as this depends on a particular
client-side configuration and ssh does not allow using a temporary $HOME.
Integrate this into "make installcheck", so that this uses gvfs-testbed when
being called as root.
https://bugzilla.gnome.org/show_bug.cgi?id=691336
Diffstat (limited to 'test')
-rw-r--r-- | test/Makefile.am | 9 | ||||
-rwxr-xr-x | test/gvfs-testbed | 172 | ||||
-rwxr-xr-x | test/test_polkitd.py | 196 |
3 files changed, 376 insertions, 1 deletions
diff --git a/test/Makefile.am b/test/Makefile.am index 1f48ebd3..68b3674e 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -67,8 +67,13 @@ check: $(CONFIG_FILES) gvfs-test $(srcdir)/run-in-tree.sh $(srcdir)/gvfs-test $(TEST_NAMES) # run tests against the installed system packages +# when running as root, use gvfs-testbed to enable all tests installcheck-local: gvfs-test - $(srcdir)/gvfs-test $(TEST_NAMES) + if [ `id -u` = 0 ]; then \ + $(srcdir)/gvfs-testbed $(srcdir)/gvfs-test $(TEST_NAMES); \ + else \ + $(srcdir)/gvfs-test $(TEST_NAMES); \ + fi CLEANFILES=$(CONFIG_FILES) @@ -76,7 +81,9 @@ EXTRA_DIST = \ benchmark-common.c \ session.conf.in \ gvfs-test \ + gvfs-testbed \ run-in-tree.sh \ + test_polkitd.py \ files/ssh_host_rsa_key files \ files/ssh_host_rsa_key.pub \ files/testcert.pem \ diff --git a/test/gvfs-testbed b/test/gvfs-testbed new file mode 100755 index 00000000..cd21dc32 --- /dev/null +++ b/test/gvfs-testbed @@ -0,0 +1,172 @@ +#!/bin/bash +# Build an "unshared tmpfs" sandbox for gvfs-test to safely run tests which +# need root privileges. +# +# (C) 2012-2013 Canonical Ltd. +# Author: Martin Pitt <martin.pitt@ubuntu.com> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. + +set -e +if [ "`id -u`" != 0 ]; then + echo "Error: this test suite wrapper needs to be called as root" >&2 + exit 1 +fi + +if ! type smbd >/dev/null 2>&1; then + echo "Error: this test suite wrapper needs samba installed" >&2 + exit 1 +fi + +# find out the user who calls us +pid=$$ +while [ "`stat -c '%u' /proc/$pid`" = "0" ]; do + pid=`awk '/^PPid:/ {print $2}' /proc/$pid/status` + if [ -z "$pid" -o "$pid" = "1" ]; then + echo "Error: Did not find a parent process that runs as non-root" >&2 + exit 1 + fi +done +CALLING_UID="`stat -c '%u' /proc/$pid`" +CALLING_USER="`stat -c '%U' /proc/$pid`" + +# sanity check +[ "$CALLING_UID" -gt 0 ] && [ -n "$CALLING_USER" ] +CALLING_GROUP="`id -gn $CALLING_USER`" + +# find udisks daemon +UDISKSD=`sed -n '/^Exec=/ { s/^[^=]*=//; p }' /usr/share/dbus-1/system-services/*UDisks2*.service | cut -f1 -d' '` +if [ ! -x "$UDISKSD" ]; then + echo "Error: Did not find udisksd path" >&2 + exit 1 +fi + +# smbd needs to be restarted in the sandbox +(service smbd stop || service smb stop) && smbd_running=1 || : +(service nmbd stop || service nmb stop) && nmbd_running=1 || : + +MNT=`mktemp -d` + +# work around scsi_debug not implementing CD-ROM SCSI commands +# see https://launchpad.net/bugs/1043182 for details +if [ -d /run/udev/rules.d/ -a ! -e /run/udev/rules.d/60-persistent-storage-scsi_debug.rules ]; then + cat <<EOF > /run/udev/rules.d/60-persistent-storage-scsi_debug.rules +KERNEL=="sr*", ENV{DISK_EJECT_REQUEST}!="?*", ATTRS{model}=="scsi_debug*", ENV{ID_CDROM_MEDIA}=="?*", IMPORT{program}="/sbin/blkid -o udev -p -u noraid \$tempnode" +EOF + sync + pkill -HUP udevd || pkill -HUP systemd-udevd +fi + +# prevent nautilus popups for temporary drives in running sessions +pkill -STOP -f gvfs-udisks2-volume-monitor || : + +cat <<EOF | unshare -m sh +set -e +mount --make-rprivate / +mount -n -t tmpfs tmpfs $MNT + +# prepare overlay directories and copy essential configuration +mkdir -p $MNT/etc/samba $MNT/var/lib/samba/private $MNT/var/cache/samba $MNT/var/log/samba $MNT/home/$CALLING_USER/run $MNT/run_samba $MNT/media +touch $MNT/etc/fstab $MNT/home/gvfs_sandbox_marker +cp -a /etc/passwd /etc/shadow /etc/group /etc/hosts /etc/pam* /etc/nsswitch.conf /etc/security/ /etc/init /etc/init.d /etc/systemd /etc/login.defs /etc/dbus-1 /etc/polkit-1 $MNT/etc/ +if [ -d /etc/selinux ]; then + cp -a /etc/selinux $MNT/etc/ +fi +chown -R $CALLING_USER:$CALLING_GROUP $MNT/home/$CALLING_USER +# ensure we can resolve our hostname +echo "127.0.0.1 `uname -n`" >> $MNT/etc/hosts + +# copy our local mock polkitd into testbed +cp `dirname $0`/test_polkitd.py $MNT/home/ + +# Debianisms +if [ -d /etc/alternatives ]; then + cp -a /etc/alternatives $MNT/etc/ +fi +if [ -L /var/run ]; then + cp -a /var/run $MNT/var +fi + +# if we run a script, we need to copy it into the sandbox as it might be in +# a directory that we overlay +if [ -f "$1" ]; then + cp -a "$1" $MNT/home/gvfs-testbed-script + ARGS="/home/gvfs-testbed-script ${@:2}" + + # we need to copy our test files as well, if we run gvfs-test + if [ -d "`dirname $1`/files" ]; then + cp -a "`dirname $1`/files" $MNT/home + fi +else + ARGS="$@" +fi + +# realize our overlays +mount -n --bind $MNT/etc/ /etc/ +mount -n --bind $MNT/home/ /home/ +mount -n --bind $MNT/var/ /var/ +mount -n --bind $MNT/media/ /media +mkdir -p /run/samba +mount -n --bind $MNT/run_samba/ /run/samba + +# run Samba with local configuration/state +cat <<SMBEOF >/etc/samba/smb.conf +[global] +workgroup = TESTGROUP +interfaces = 127.0.0.0/8 +map to guest = Bad User + +[public] +path = /home/$CALLING_USER/public +guest ok = yes + +[private] +path = /home/$CALLING_USER/private +read only = no +SMBEOF +nmbd -D -l /var/log/samba +smbd -D -l /var/log/samba + +# we need a predictable password for the smb:// authenticated test, so change +# it to "foo" in the sandbox +/bin/echo -e 'foo\\nfoo\\n' | smbpasswd -a $CALLING_USER -s + +# set up SSH key for test user +su -lc "mkdir ~/.ssh; ssh-keygen -q -f ~/.ssh/id_rsa -N ''" $CALLING_USER + +# create a root shell that the user can call to control scsi_debug and +# similar +cp /bin/sh /home/$CALLING_USER/rootsh +chown root:$CALLING_USER /home/$CALLING_USER/rootsh +chmod 4550 /home/$CALLING_USER/rootsh + +# we must start udisksd in our private mount environment, so that gvfs and +# udisks agree to the same view of mounts +$UDISKSD --no-debug --replace & +UDISKS_PID=\$! + +echo "Running commmand in testbed: \$ARGS" +su -lc "export PATH=$PATH; export \\\`dbus-launch\\\`; export XDG_RUNTIME_DIR=/home/$CALLING_USER/run; \$ARGS; rc=\\\$?; kill \\\$DBUS_SESSION_BUS_PID; exit \\\$rc" $CALLING_USER || { + RC=\$? + echo "=== command failed, showing Samba log files ===" + for f in /var/log/samba/log.*; do + echo "--- \$f ---" + cat \$f + done +} +(cat /var/run/samba/*.pid | xargs kill ) || ( cat /var/run/[sn]mbd.pid | xargs kill ) +kill \$UDISKS_PID || : +exit \$RC +EOF +RC=$? + +pkill -CONT -f gvfs-udisks2-volume-monitor || : + +[ -n "$smbd_running" ] && service smbd start || service smb start || : +[ -n "$nmbd_running" ] && service nmbd start || service nmb start || : + +rmdir "$MNT" || : +exit $RC diff --git a/test/test_polkitd.py b/test/test_polkitd.py new file mode 100755 index 00000000..a66c4266 --- /dev/null +++ b/test/test_polkitd.py @@ -0,0 +1,196 @@ +#!/usr/bin/python3 +# (C) 2011 Sebastian Heinlein +# (C) 2012 Canonical Ltd. +# Authors: +# Sebastian Heinlein <sebi@glatzor.de> +# Martin Pitt <martin.pitt@ubuntu.com> +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. + +'''Simple mock polkit daemon for test suites. + +This also provides some convenience API for launching the daemon and for +writing unittest test cases involving polkit operations. +''' + +import sys +import os +import argparse +import unittest +import signal +import time + +import dbus +import dbus.service +from gi.repository import GLib, Gio + +# ---------------------------------------------------------------------------- + +class TestPolicyKitDaemon(dbus.service.Object): + def __init__(self, allowed_actions, on_bus=None, replace=False): + '''Initialize test polkit daemon. + + @allowed_actions is a list of PolicyKit action IDs which will be + allowed (active/inactive sessions or user IDs will not be considered); + all actions not in that list will be denied. If 'all' is an element of + @allowed_actions, all actions will be allowed. + + When @on_bus string is given, the daemon will run on that D-BUS + address, otherwise on the system D-BUS. + + If @replace is True, this will replace an already running polkit daemon + on the D-BUS. + ''' + self.allowed_actions = allowed_actions + if on_bus: + bus = dbus.bus.BusConnection(on_bus) + else: + bus = dbus.SystemBus() + bus_name = dbus.service.BusName('org.freedesktop.PolicyKit1', + bus, do_not_queue=True, + replace_existing=replace, + allow_replacement=True) + bus.add_signal_receiver(self.on_disconnected, signal_name='Disconnected') + + dbus.service.Object.__init__(self, bus_name, + '/org/freedesktop/PolicyKit1/Authority') + self.loop = GLib.MainLoop() + + def run(self): + self.loop.run() + + @dbus.service.method('org.freedesktop.PolicyKit1.Authority', + in_signature='(sa{sv})sa{ss}us', + out_signature='(bba{ss})') + def CheckAuthorization(self, subject, action_id, details, flags, + cancellation_id): + if 'all' in self.allowed_actions: + allowed = True + else: + allowed = action_id in self.allowed_actions + challenged = False + details = {'test': 'test'} + return (allowed, challenged, details) + + @dbus.service.method('org.freedesktop.PolicyKit1.Authority', + in_signature='', out_signature='') + def Quit(self): + GLib.idle_add(self.loop.quit) + + def on_disconnected(self): + print('disconnected from D-BUS, terminating') + self.Quit() + +# ---------------------------------------------------------------------------- + +class PolkitTestCase(unittest.TestCase): + '''Convenient test cases involving polkit. + + Call start_polkitd() with the list of allowed actions in your test cases. + The daemon will be automatically terminated when the test case exits. + ''' + + def __init__(self, methodName='runTest'): + unittest.TestCase.__init__(self, methodName) + self.polkit_pid = None + + def start_polkitd(self, allowed_actions, on_bus=None): + '''Start test polkitd. + + This should be called in your test cases before the exercised code + makes any polkit query. The daemon will be stopped automatically when + the test case ends (regardless of whether its successful or failed). If + you want to test multiple different action sets in one test case, you + have to call stop_polkitd() before starting a new one. + + @allowed_actions is a list of PolicyKit action IDs which will be + allowed (active/inactive sessions or user IDs will not be considered); + all actions not in that list will be denied. If 'all' is an element of + @allowed_actions, all actions will be allowed. + + When @on_bus string is given, the daemon will run on that D-BUS + address, otherwise on the system D-BUS. + ''' + assert self.polkit_pid is None, \ + 'can only launch one polkitd at a time; write a separate test case or call stop_polkitd()' + self.polkit_pid = spawn(allowed_actions, on_bus) + self.addCleanup(self.stop_polkitd) + + def stop_polkitd(self): + '''Stop test polkitd. + + This happens automatically when a test case ends, but is required when + you want to test multiple different action sets in one test case. + ''' + assert self.polkit_pid is not None, 'polkitd is not running' + os.kill(self.polkit_pid, signal.SIGTERM) + os.waitpid(self.polkit_pid, 0) + self.polkit_pid = None + +# ---------------------------------------------------------------------------- + +def _run(allowed_actions, bus_address, replace=False): + # Set up the DBus main loop + import dbus.mainloop.glib + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + + polkitd = TestPolicyKitDaemon(allowed_actions, bus_address, replace) + polkitd.run() + +def spawn(allowed_actions, on_bus=None): + '''Run a TestPolicyKitDaemon instance in a separate process. + + @allowed_actions is a list of PolicyKit action IDs which will be + allowed (active/inactive sessions or user IDs will not be considered); + all actions not in that list will be denied. If 'all' is an element of + @allowed_actions, all actions will be allowed. + + When @on_bus string is given, the daemon will run on that D-BUS address, + otherwise on the system D-BUS. + + The daemon will terminate automatically when the @on_bus D-BUS goes down. + If that does not happen (e. g. you test on the actual system/session bus), + you need to kill it manually. + + Returns the process ID of the spawned daemon. + ''' + pid = os.fork() + if pid == 0: + # child + _run(allowed_actions, on_bus) + os._exit(0) + + # wait until the daemon is up on the bus + if on_bus: + bus = dbus.bus.BusConnection(on_bus) + elif 'DBUS_SYSTEM_BUS_ADDRESS' in os.environ: + # dbus.SystemBus() does not recognize this env var, so we have to + # handle that manually + bus = dbus.bus.BusConnection(os.environ['DBUS_SYSTEM_BUS_ADDRESS']) + else: + bus = dbus.SystemBus() + timeout = 50 + while timeout > 0 and not bus.name_has_owner('org.freedesktop.PolicyKit1'): + timeout -= 1 + time.sleep(0.1) + assert timeout > 0, 'test polkitd failed to start up' + + return pid + +def main(): + parser = argparse.ArgumentParser(description='Simple mock polkit daemon for test suites') + parser.add_argument('-a', '--allowed-actions', metavar='ACTION[,ACTION,...]', + default='', help='Comma separated list of allowed action ids') + parser.add_argument('-b', '--bus-address', + help='D-BUS address to listen on (if not given, listen on system D-BUS)') + parser.add_argument('-r', '--replace', action='store_true', + help='Replace existing polkit daemon on the bus') + args = parser.parse_args() + + _run(args.allowed_actions.split(','), args.bus_address, args.replace) + +if __name__ == '__main__': + main() |