[gvfs] Add gvfs-testbed to enable tests which need root



commit 7b331d0c8331f577311a3d94a4651e8fdfa7a19f
Author: Martin Pitt <martinpitt gnome org>
Date:   Tue Jan 8 11:08:02 2013 +0100

    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

 test/Makefile.am     |    9 ++-
 test/gvfs-testbed    |  172 +++++++++++++++++++++++++++++++++++++++++++
 test/test_polkitd.py |  196 ++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 376 insertions(+), 1 deletions(-)
---
diff --git a/test/Makefile.am b/test/Makefile.am
index 1f48ebd..68b3674 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 0000000..cd21dc3
--- /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 0000000..a66c426
--- /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()


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]