diff --git a/bin/CryptoBoxRootActions b/bin/CryptoBoxRootActions index 2525c3f..f04a600 100755 --- a/bin/CryptoBoxRootActions +++ b/bin/CryptoBoxRootActions @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Copyright 2006 sense.lab e.V. +# Copyright 2007 sense.lab e.V. # # This file is part of the CryptoBox. # @@ -20,13 +20,23 @@ # -"""module for executing the programs, that need root privileges +"""module for executing some programs or scripts that need root privileges Syntax: - - TODO + check + - return exitcode zero if basic checks succeeded -this script will always return with an exitcode 0 (true), -if "check" is the only argument + program PROGRAM_NAME [ARGS] + - call the program (must be defined in "allowedProgs" below) + + event EVENT_SCRIPT [ARGS] + - call an event script + + plugin PLUGIN_NAME [ARGS] + - call a root_action script of a plugin + +For more detailed information take a look at the manpage: + "man CryptoBoxRootActions" """ __revision__ = "$Id" @@ -44,6 +54,7 @@ allowedProgs = { "mount": "/bin/mount", "umount": "/bin/umount", "blkid": "/sbin/blkid", + "pvdisplay": "/sbin/pvdisplay", } ## this line is necessary for running unittests or playing around with a local @@ -387,6 +398,19 @@ def run_umount(args): return proc.returncode == 0 +def run_pvdisplay(args): + """execute pvdisplay to check for physical LVM devices + """ + if len(args) > 0: + raise "WrongArguments", "no arguments may be supplied for 'pvdisplay'" + ## call pvdisplay with the parameter "--colon" + proc = subprocess.Popen( + shell = False, + args = [ allowedProgs["pvdisplay"], "--colon" ]) + proc.wait() + return proc.returncode == 0 + + def getCallingUserInfo(): """return information about the user that was calling this program via "super" @@ -456,6 +480,7 @@ if __name__ == "__main__": # exit silently sys.exit(0) + ## call a plugin root_action script if args[0].lower() == "plugin": del args[0] try: @@ -468,6 +493,7 @@ if __name__ == "__main__": else: sys.exit(1) + ## call an event script if args[0].lower() == "event": del args[0] try: @@ -480,11 +506,13 @@ if __name__ == "__main__": else: sys.exit(1) - # check parameters count - if len(args) < 2: - sys.stderr.write("Not enough arguments supplied (%s)!\n" % " ".join(args)) - sys.exit(100) + ## call one of the allowed programs + if args[0].lower() == "program": + del args[0] + if len(args) < 1: + sys.stderr.write("No program specified for execution\n") + sys.exit(100) progRequest = args[0] del args[0] @@ -495,6 +523,7 @@ if __name__ == "__main__": if progRequest == "cryptsetup": runner = run_cryptsetup elif progRequest == "mount": runner = run_mount elif progRequest == "umount": runner = run_umount + elif progRequest == "pvdisplay": runner = run_pvdisplay else: sys.stderr.write("The interface for this program (%s) is " \ + "not yet implemented!\n" % progRequest) diff --git a/man/CryptoBoxRootActions.8 b/man/CryptoBoxRootActions.8 index 6ad7359..6a9ef56 100644 --- a/man/CryptoBoxRootActions.8 +++ b/man/CryptoBoxRootActions.8 @@ -1,4 +1,4 @@ -.TH CryptoBoxRootActions 8 "March 02007" "CryptoBox" "CryptoBox-Server manual" +.TH CryptoBoxRootActions 8 "August 02007" "CryptoBox" "CryptoBox-Server manual" .SH NAME CryptoBoxRootActions \- The CryptoBoxWebserver calls this script in order to execute various programs which require root privileges. @@ -13,7 +13,7 @@ plugin \fIFEATURE_SCRIPT\fR [\fIARGS\fR] hook \fIEVENT_SCRIPT\fR [\fIARGS\fR] .br .B CryptoBoxRootActions -\fIPROG\fR [\fIARGS\fR] +program \fIPROG\fR [\fIARGS\fR] .SH DESCRIPTION CryptoBoxRootActions is a script that is called by the \fBCryptoBox\fR-Server to execute programs which require root privileges. You @@ -30,7 +30,8 @@ CryptoBoxRootActions /usr/sbin/CryptoBoxRootActions cryptobox .PP We assume that the CryptoBoxRootActions script is located at \fI/usr/sbin/CryptoBoxRootActions\fR. Furthermore the user running the -CryptoBox-Server is assumed to be \fIcryptobox\fR. +CryptoBox-Server is assumed to be \fIcryptobox\fR. Otherwise you must change the +above line accordingly. .SH CONFIGURATION CHECK Call the CryptoBoxRootActions script with the argument \fIcheck\fR to test if \fBsuper\fR is configured properly. Just type the following: diff --git a/src/cryptobox/core/blockdevice.py b/src/cryptobox/core/blockdevice.py index a71848d..92e2644 100644 --- a/src/cryptobox/core/blockdevice.py +++ b/src/cryptobox/core/blockdevice.py @@ -25,26 +25,44 @@ These classes detect and filter available blockdevices. __revision__ = "$Id$" -""" -TODO: - - implement some caching - - find the devnodes for each device (e.g. /dev/hda) - - detect luks devices - - detect LVM -""" - +#TODO: use logger to report interesting behaviour import os +import subprocess +import time +import cryptobox.core.settings + + +DEFAULT_SYSBLOCK_DIR = '/sys/block' +DEFAULT_DEVNODE_DIR = '/dev' +MINIMUM_STORAGE_SIZE = 20 +MAJOR_DEVNUM_RAM = 1 +MAJOR_DEVNUM_LOOP = 7 +MAJOR_DEVNUM_MD_RAID = 9 + +USE_CACHE = True +CACHE_EXPIRE_SECONDS = 60 + +#TODO: remove this after profiling +IS_VISIBLE = True + +## caching is quite important for the following implementation +CACHED_VALUES = {} class Blockdevices: + """handle all blockdevices of this system + """ - def __init__(self, sysblock_dir='/sys/block', devnode_dir='/dev'): + def __init__(self, + sysblock_dir=DEFAULT_SYSBLOCK_DIR, + devnode_dir=DEFAULT_DEVNODE_DIR): self.sysblock_dir = sysblock_dir self.devnode_dir = devnode_dir self.devices = [] for devdir in find_blockdevices(self.sysblock_dir): - blockdevice = Blockdevice(devdir, self.devnode_dir) - if not blockdevice is None: + blockdevice = get_blockdevice(devdir, + self.sysblock_dir, self.devnode_dir) + if (not blockdevice is None) and blockdevice.is_valid(): self.devices.append(blockdevice) @@ -55,60 +73,197 @@ class Blockdevices: - class Blockdevice: - def __init__(self, dev, devnode_dir='/dev', sysblock_dir='/sys/block'): + def __init__(self, dev, + sysblock_dir=DEFAULT_SYSBLOCK_DIR, + devnode_dir=DEFAULT_DEVNODE_DIR): + """initialize the blockdevice + """ + self.devdir = dev self.devnode_dir = devnode_dir self.sysblock_dir = sysblock_dir - if os.path.isabs(dev): - self.devdir = dev - else: - self.devdir = self.__find_relative_device(dev) - if self.devdir is None: - self = None - return self.name = os.path.basename(self.devdir) self.devnum = self.__get_major_minor() - ## check valid devnum - try: - major, minor = self.devnum - except TypeError: - self = None - return self.size = self.__get_size() self.range = self.__get_device_range() self.slaves = self.__get_dev_related("slaves") self.holders = self.__get_dev_related("holders") self.children = self.__get_children() + self.devnodes = self.__get_device_nodes() - def isstorage(self): - if self.range > 1: - ## partitionable blockdevice + def is_valid(self): + """ check if the device is usable and valid + """ + if not self.devnodes: return False - if self.size < 20: - ## extended partition, unused loop device - return False - if self.devnum[0] == 1: - ## ram device - return False - if self.children: - ## a parent blockdevice + ## check valid devnum + try: + major, minor = self.devnum + if (major == 0) and (minor == 0): + return False + ## ram devices are ignored + if major == MAJOR_DEVNUM_RAM: + return False + ## loop devices are ignored + if major == MAJOR_DEVNUM_LOOP: + return False + except TypeError: return False return True - def ispartitionable(self): + def is_storage(self): + """return if this device can be used as a storage + """ + ## check the cache first + cache_link = ["blockdevice_info", self.name, "is_storage"] + cached = _get_cached_value(cache_link) + if not cached is None: + return cached + + if self.range > 1: + ## partitionable blockdevice + _set_cached_value(cache_link, False) + return False + if self.size < MINIMUM_STORAGE_SIZE: + ## extended partition, unused loop device + _set_cached_value(cache_link, False) + return False + if self.devnum[0] == MAJOR_DEVNUM_RAM: + ## ram device + _set_cached_value(cache_link, False) + return False + ## are we the device mapper of a luks device? + for slave in self.slaves: + if get_blockdevice(slave, self.sysblock_dir, + self.devnode_dir).is_luks(): + _set_cached_value(cache_link, False) + return False + ## if we are a luks device with exactly one child, then + ## we are a storage + if (len(self.children) == 1) and self.is_luks(): + _set_cached_value(cache_link, True) + return True + if self.children: + ## a parent blockdevice + _set_cached_value(cache_link, False) + return False + _set_cached_value(cache_link, True) + return True + + + def is_partitionable(self): + """is the device partitionable + """ if self.range > 1: return True + else: + return False - def __find_relative_device(self, devname): - for devdir in find_blockdevices(self.sysblock_dir): - if os.path.basename(devdir) == devname: - return devdir - return None + def is_lvm_pv(self): + """return if the device is a physical volume of a LVM + """ + ## check the cache first + cache_link = ["blockdevice_info", self.name, "is_lvm_pv"] + cached = _get_cached_value(cache_link) + if not cached is None: + return cached + + ## is one of the devnodes of the device a physical volume? + for one_lvm_pv in find_lvm_pv(): + if one_lvm_pv in self.devnodes: + _set_cached_value(cache_link, True) + return True + _set_cached_value(cache_link, False) + return False + + + def is_lvm_lv(self): + """return if the device is a logical volume of a LVM + """ + ## check the cache first + cache_link = ["blockdevice_info", self.name, "is_lvm_lv"] + cached = _get_cached_value(cache_link) + if not cached is None: + return cached + + ## is one of the devnodes of the device a physical volume? + ## logical LVM volumes always depend on their physical volumes + if not self.slaves: + _set_cached_value(cache_link, False) + return False + ## is one of the LVM physical volumes a device node of our slave(s)? + for one_lvm_pv in find_lvm_pv(): + for one_slave in self.slaves: + if one_lvm_pv in get_blockdevice(one_slave, + self.sysblock_dir, self.devnode_dir).devnodes: + _set_cached_value(cache_link, True) + return True + _set_cached_value(cache_link, False) + return False + + + def is_md_raid(self): + """check if the device is the base of a md raid device + """ + ## check the cache first + cache_link = ["blockdevice_info", self.name, "is_md_raid"] + cached = _get_cached_value(cache_link) + if not cached is None: + return cached + + if self.range > 1: + result = False + elif self.size < MINIMUM_STORAGE_SIZE: + result = False + else: + for hold in self.holders: + if get_blockdevice(hold, self.sysblock_dir, + self.devnode_dir).devnum[0] == MAJOR_DEVNUM_MD_RAID: + result = True + break + else: + result = False + + ## store result and return + _set_cached_value(cache_link, result) + return result + + + def is_luks(self): + """check if the device is a luks container + """ + ## check the cache first + cache_link = ["blockdevice_info", self.name, "is_luks"] + cached = _get_cached_value(cache_link) + if not cached is None: + return cached + + if self.range > 1: + result = False + elif self.size < MINIMUM_STORAGE_SIZE: + result = False + elif self.is_lvm_pv(): + result = False + elif self.is_md_raid(): + result = False + else: + ## is the device a luks volume? + prefs = _load_preferences() + proc = subprocess.Popen( + shell = False, + stdout = subprocess.PIPE, + stderr = subprocess.PIPE, + args = [ prefs["Programs"]["cryptsetup"], + "--batch-mode", "isLuks", self.devnodes[0]]) + proc.wait() + result = proc.returncode == 0 + ## store result and return + _set_cached_value(cache_link, result) + return result def __get_dev_related(self, subdir): @@ -121,6 +276,8 @@ class Blockdevice: def __get_size(self): + """return the size (in kB) of the blockdevice + """ default = 0 try: return int(file(os.path.join(self.devdir, 'size')).read()) @@ -132,12 +289,15 @@ class Blockdevice: def __get_major_minor(self): """return the major and minor of the device""" - default = (0 ,0) + default = (0, 0) try: content = file(os.path.join(self.devdir, "dev")).read() except IOError: return default - major, minor = content.split(":", 2) + try: + major, minor = content.split(":", 1) + except TypeError: + return default try: return int(major), int(minor) except ValueError: @@ -165,20 +325,48 @@ class Blockdevice: all holders, subdevices and children of subdevices """ - direct_children = [Blockdevice(child).name + direct_children = [ + get_blockdevice(child, self.sysblock_dir, self.devnode_dir).name for child in find_blockdevices(self.devdir)] direct_children.extend(self.holders[:]) children = direct_children[:] for dchild in direct_children: - children.extend(Blockdevice(dchild).children) + children.extend(get_blockdevice(dchild, self.sysblock_dir, + self.devnode_dir).children) return children + def __get_device_nodes(self): + """get all device nodes with the major/minor combination of the device + """ + result = [] + major, minor = self.devnum + + def find_major_minor(arg, dirname, fnames): + for fname in fnames: + try: + stat = os.stat(os.path.join(dirname, fname)) + ## check if it is a blockdevice and compare major/minor + if (stat.st_mode & 060000 == 060000) \ + and (os.major(stat.st_rdev) == major) \ + and (os.minor(stat.st_rdev) == minor): + result.append(os.path.join(dirname, fname)) + except OSError: + pass + + os.path.walk(self.devnode_dir, find_major_minor, None) + return result + + def __str__(self): + """display the name of the device + """ return self.name def info(self): + """display some information about the device + """ output = "%s:\n" % self.name output += "\t%s:\t%s\n" % ("blockdir", self.devdir) output += "\t%s:\t%s\n" % ("major/minor", self.devnum) @@ -187,12 +375,45 @@ class Blockdevice: output += "\t%s:\t\t%s\n" % ("slaves", self.slaves) output += "\t%s:\t%s\n" % ("holders", self.holders) output += "\t%s:\t%s\n" % ("children", self.children) + output += "\t%s:\t%s\n" % ("device nodes", self.devnodes) + output += "\tflags:\t\t" + for funcname in [ "storage", "md_raid", "partitionable", "luks", + "lvm_pv", "lvm_lv"]: + if getattr(self, "is_%s" % funcname)(): + output += "%s " % funcname + output += "\n" return output +def get_blockdevice(dev, + sysblock_dir=DEFAULT_SYSBLOCK_DIR, + devnode_dir=DEFAULT_DEVNODE_DIR): + if os.path.isabs(dev): + if os.path.isfile(os.path.join(dev, "dev")): + devdir = dev + else: + return None + else: + for one_devdir in find_blockdevices(sysblock_dir): + if os.path.basename(one_devdir) == dev: + devdir = one_devdir + break + else: + return None + devname = os.path.basename(devdir) + dev = _get_cached_value(["blockdevices", devname]) + if dev is None: + dev = Blockdevice(devdir, sysblock_dir, devnode_dir) + _set_cached_value(["blockdevices", devname], dev) + return dev + def find_blockdevices(top_dir): + cached = _get_cached_value(["blockdevice_dirs", top_dir]) + if not cached is None: + return cached[:] + dev_dirs = [] def look4dev_dirs(arg, dirname, fnames): @@ -210,21 +431,141 @@ def find_blockdevices(top_dir): fnames.remove(fname) os.path.walk(top_dir, look4dev_dirs, 'dev') - return dev_dirs + _set_cached_value(["blockdevice_dirs", top_dir], dev_dirs) + return dev_dirs[:] + + +def find_lvm_pv(): + """return the blockdevice names of all physical LVM volumes + """ + cached = _get_cached_value(["lvm", "pv"]) + if not cached is None: + return cached[:] + + #TODO: should we check, if LVM is supported at all? + # e.g. by checking the existence of pvdisplay? + prefs = _load_preferences() + result = None + try: + proc = subprocess.Popen( + shell = False, + stdout = subprocess.PIPE, + args = [ prefs["Programs"]["super"], + prefs["Programs"]["CryptoBoxRootActions"], + "program", "pvdisplay" ]) + proc.wait() + except OSError, err_msg: + # TODO: add a logging warning + result = [] + if proc.returncode != 0: + # TODO: add a logging warning + result = [] + if result is None: + result = [] + for line in proc.stdout.readlines(): + result.append(line.split(":", 1)[0].strip()) + _set_cached_value(["lvm", "pv"], result) + return result[:] + + +def _get_cached_value(link): + """return a cached value + + "link" is an array of the hierachie of the accessed item + e.g. link = ["blockdevices", "hda"] + return None if the value is not in the cache or if USE_CACHE is False + """ + if not USE_CACHE: + return None + + if "expires" in CACHED_VALUES: + if CACHED_VALUES["expires"] < int(time.time()): + reset_cache() + else: + __reset_cache_timer() + + ref = CACHED_VALUES + for element in link: + if element in ref: + ref = ref[element] + else: + return None + return ref + + +def reset_cache(): + ## refresh the cache + for item in CACHED_VALUES: + CACHED_VALUES[item] = {} + __reset_cache_timer() + + +def __reset_cache_timer(): + CACHED_VALUES["expires"] = int(time.time()) + CACHE_EXPIRE_SECONDS + + + +def _set_cached_value(link, item): + """store an item in the cache + + "link" is an array of the hierachie of the accessed item + e.g. link = ["blockdevices", "hda"] + """ + if not USE_CACHE: + return + ref = CACHED_VALUES + for element in link[:-1]: + if not element in ref: + ## create a non-existing sub element + ref[element] = {} + ref = ref[element] + ## store the item + ref[link[-1]] = item + + +def _load_preferences(): + prefs = cryptobox.core.settings.get_current_settings() + if not prefs is None: + ## now the preferences are loaded + return prefs + ## we have to load an emergency fallback for proper function + ## this is mainly useful for local testing + root_dir = os.path.realpath(os.path.join(globals()["cryptobox"].__path__[0], + os.path.pardir, os.path.pardir)) + config_file = os.path.join(root_dir, "bin", "cryptobox.conf") + ## we have to chdir to the 'bin' directory - otherwise the paths in + ## cryptobox.conf do not work + os.chdir(os.path.dirname(config_file)) + return cryptobox.core.settings.CryptoBoxSettings(config_file) + if __name__ == '__main__': - blocks = Blockdevices() - for dev in blocks.get_devices(): - print dev.info() - print - print "Usable storage devices:" - for dev in blocks.get_devices(): - if dev.isstorage(): - print dev - print - print "Partitionable devices:" - for dev in blocks.get_devices(): - if dev.ispartitionable(): - print dev + ## list the properties of all available devices + ## this is just for testing purposes + blocks = Blockdevices().get_devices() + + ## do we want to show the result? + def show(text=""): + if IS_VISIBLE: + print text + + if len(blocks) > 0: + ## show all devices and their properties + show("Properties of all devices:") + for device in blocks: + show(device.info()) + + ## discover all self-check methods + example = blocks[0] + flag_checker = [ method for method in dir(example) + if callable(getattr(example, method)) + and method.startswith("is_")] + ## list all checks and the respective devices + for check in flag_checker: + show("List of '%s' devices:" % check[3:]) + for device in blocks: + if getattr(device, check)(): + show("\t%s" % device) + show() diff --git a/src/cryptobox/core/container.py b/src/cryptobox/core/container.py index 7c6d434..1890f96 100644 --- a/src/cryptobox/core/container.py +++ b/src/cryptobox/core/container.py @@ -275,7 +275,7 @@ class CryptoBoxContainer: args = [ self.cbox.prefs["Programs"]["super"], self.cbox.prefs["Programs"]["CryptoBoxRootActions"], - "cryptsetup", + "program", "cryptsetup", "luksAddKey", self.device, "--batch-mode"]) @@ -523,7 +523,7 @@ class CryptoBoxContainer: args = [ self.cbox.prefs["Programs"]["super"], self.cbox.prefs["Programs"]["CryptoBoxRootActions"], - "cryptsetup", + "program", "cryptsetup", "luksOpen", self.device, self.name, @@ -542,7 +542,7 @@ class CryptoBoxContainer: args = [ self.cbox.prefs["Programs"]["super"], self.cbox.prefs["Programs"]["CryptoBoxRootActions"], - "mount", + "program", "mount", os.path.join(self.__dmDir, self.name), self.__get_mount_point()]) proc.wait() @@ -572,7 +572,7 @@ class CryptoBoxContainer: args = [ self.cbox.prefs["Programs"]["super"], self.cbox.prefs["Programs"]["CryptoBoxRootActions"], - "umount", + "program", "umount", self.__get_mount_point()]) proc.wait() if proc.returncode != 0: @@ -588,7 +588,7 @@ class CryptoBoxContainer: args = [ self.cbox.prefs["Programs"]["super"], self.cbox.prefs["Programs"]["CryptoBoxRootActions"], - "cryptsetup", + "program", "cryptsetup", "luksClose", self.name, "--batch-mode"]) @@ -620,7 +620,7 @@ class CryptoBoxContainer: args = [ self.cbox.prefs["Programs"]["super"], self.cbox.prefs["Programs"]["CryptoBoxRootActions"], - "mount", + "program", "mount", self.device, self.__get_mount_point()]) proc.wait() @@ -653,7 +653,7 @@ class CryptoBoxContainer: args = [ self.cbox.prefs["Programs"]["super"], self.cbox.prefs["Programs"]["CryptoBoxRootActions"], - "umount", + "program", "umount", self.__get_mount_point()]) proc.wait() if proc.returncode != 0: @@ -734,7 +734,7 @@ class CryptoBoxContainer: args = [ self.cbox.prefs["Programs"]["super"], self.cbox.prefs["Programs"]["CryptoBoxRootActions"], - "cryptsetup", + "program", "cryptsetup", "luksFormat", self.device, "--batch-mode", @@ -755,7 +755,7 @@ class CryptoBoxContainer: args = [ self.cbox.prefs["Programs"]["super"], self.cbox.prefs["Programs"]["CryptoBoxRootActions"], - "cryptsetup", + "program", "cryptsetup", "luksOpen", self.device, self.name, diff --git a/src/cryptobox/core/settings.py b/src/cryptobox/core/settings.py index 5cc074e..3b3bb69 100644 --- a/src/cryptobox/core/settings.py +++ b/src/cryptobox/core/settings.py @@ -40,6 +40,19 @@ VOLUMESDB_FILE = "cryptobox_volumes.db" PLUGINCONF_FILE = "cryptobox_plugins.conf" USERDB_FILE = "cryptobox_users.db" +## allow to retrieve the most recently created setting object +CURRENT_SETTING = [] + + +def get_current_settings(): + """return the most recently created setting object + """ + if not CURRENT_SETTING: + return None + else: + return CURRENT_SETTING[0] + + class CryptoBoxSettings: """Manage the various configuration files of the CryptoBox @@ -63,6 +76,7 @@ class CryptoBoxSettings: self.misc_files = [] self.reload_misc_files() self.__is_initialized = True + CURRENT_SETTING.insert(0, self) def reload_misc_files(self): @@ -190,7 +204,7 @@ class CryptoBoxSettings: args = [ self.prefs["Programs"]["super"], self.prefs["Programs"]["CryptoBoxRootActions"], - "mount", + "program", "mount", "_tmpfs_", mount_dir ]) (stdout, stderr) = proc.communicate() @@ -211,7 +225,7 @@ class CryptoBoxSettings: args = [ self.prefs["Programs"]["super"], self.prefs["Programs"]["CryptoBoxRootActions"], - "mount", + "program", "mount", partition, mount_dir ]) (stdout, stderr) = proc.communicate() @@ -241,7 +255,7 @@ class CryptoBoxSettings: args = [ self.prefs["Programs"]["super"], self.prefs["Programs"]["CryptoBoxRootActions"], - "umount", + "program", "umount", mount_dir ]) (stdout, stderr) = proc.communicate() if proc.returncode != 0: