From 0c23c5e2c3c347206e1338e78a838f323ee61c61 Mon Sep 17 00:00:00 2001 From: lars Date: Mon, 6 Nov 2006 06:16:27 +0000 Subject: [PATCH] fixed: name of volume is persistent after format fixed: use 'cryptsetup luksUUID' to retrieve UUIDs of luks devices added: analyse request URL andd add information to hdf dataset added: attribute 'device' is persistent (like 'weblang') to allow redirects between volume plugins changed: moved URLs of plugins from 'plugins/???' to '???' (flat hierarchie -> simple links) added: new action for all plugins: "redirect" - call specific plugin after completing the current one fixed: permission fix for vfat filesystems during mount / for ext2/3: after mount regular checks for config partition (mount, if it becomes available) --- pythonrewrite/bin/CryptoBox.py | 10 +++ pythonrewrite/bin/CryptoBoxContainer.py | 63 ++++++++++---- pythonrewrite/bin/CryptoBoxRootActions.py | 47 ++++++++-- pythonrewrite/bin/CryptoBoxSettings.py | 18 ++-- pythonrewrite/bin/WebInterfaceDataset.py | 72 +++++++++++----- pythonrewrite/bin/WebInterfaceSites.py | 100 +++++++++++++--------- pythonrewrite/bin/cryptobox.conf | 7 +- pythonrewrite/bin/unittests.CryptoBox.py | 2 +- 8 files changed, 225 insertions(+), 94 deletions(-) diff --git a/pythonrewrite/bin/CryptoBox.py b/pythonrewrite/bin/CryptoBox.py index 9d245a2..1472b53 100755 --- a/pythonrewrite/bin/CryptoBox.py +++ b/pythonrewrite/bin/CryptoBox.py @@ -122,6 +122,7 @@ class CryptoBoxProps(CryptoBox): def reReadContainerList(self): + self.log.debug("rereading container list") self.containers = [] for device in CryptoBoxTools.getAvailablePartitions(): if self.isDeviceAllowed(device) and not self.isConfigPartition(device): @@ -246,6 +247,14 @@ class CryptoBoxProps(CryptoBox): if self.prefs.nameDB[key] == name: return key "the uuid was not found" return None + + + def removeUUID(self, uuid): + if uuid in self.prefs.nameDB.keys(): + del self.prefs.nameDB[uuid] + return True + else: + return False def getAvailableLanguages(self): @@ -261,6 +270,7 @@ class CryptoBoxProps(CryptoBox): + if __name__ == "__main__": cb = CryptoBoxProps() diff --git a/pythonrewrite/bin/CryptoBoxContainer.py b/pythonrewrite/bin/CryptoBoxContainer.py index 58bf850..e658c53 100755 --- a/pythonrewrite/bin/CryptoBoxContainer.py +++ b/pythonrewrite/bin/CryptoBoxContainer.py @@ -120,15 +120,18 @@ class CryptoBoxContainer: def create(self, type, password=None): + old_name = self.getName() if type == self.Types["luks"]: self.__createLuks(password) - self.resetObject() - return - if type == self.Types["plain"]: + elif type == self.Types["plain"]: self.__createPlain() - self.resetObject() - return - raise CBInvalidType("invalid container type (%d) supplied" % (type, )) + else: + raise CBInvalidType("invalid container type (%d) supplied" % (type, )) + ## no exception was raised during creation -> we can continue + ## reset the properties (encryption state, ...) of the device + self.resetObject() + ## restore the old name (must be after resetObject) + self.setName(old_name) def changePassword(self, oldpw, newpw): @@ -215,16 +218,42 @@ class CryptoBoxContainer: def __getUUID(self): - """return UUID for luks partitions, ext2/3 and vfat filesystems""" - emergency_default = self.device.replace(os.path.sep, "_") - devnull = None + if self.__getTypeOfPartition() == self.Types["luks"]: + guess = self.__getLuksUUID() + else: + guess = self.__getNonLuksUUID() + ## did we get a valid value? + if guess: + return guess + else: + ## emergency default value + return self.device.replace(os.path.sep, "_") + + + def __getLuksUUID(self): + """get uuid for luks devices""" + proc = subprocess.Popen( + shell = False, + stdout = subprocess.PIPE, + stderr = subprocess.PIPE, + args = [self.cbox.prefs["Programs"]["cryptsetup"], + "luksUUID", + self.device]) + (stdout, stderr) = proc.communicate() + if proc.returncode != 0: + self.cbox.log.info("could not retrieve luks uuid (%s): %s", (self.device, stderr.strip())) + return None + return stdout.strip() + + + def __getNonLuksUUID(self): + """return UUID for ext2/3 and vfat filesystems""" try: devnull = open(os.devnull, "w") except IOError: self.warn("Could not open %s" % (os.devnull, )) proc = subprocess.Popen( shell=False, - stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, args=[self.cbox.prefs["Programs"]["blkid"], @@ -233,14 +262,14 @@ class CryptoBoxContainer: "-c", os.devnull, "-w", os.devnull, self.device]) - proc.wait() - result = proc.stdout.read().strip() - if proc.returncode != 0: - self.log.warn("retrieving of partition type via 'blkid' failed: %s" % (proc.stderr.read().strip(), )) - return emergency_default + (stdout, stderr) = proc.communicate() devnull.close() - if result: return result - return emergency_default + ## execution failed? + if proc.returncode != 0: + self.log.info("retrieving of partition type (%s) via 'blkid' failed: %s - maybe it is encrypted?" % (self.device, stderr.strip())) + return None + ## return output of blkid + return stdout.strip() def __getTypeOfPartition(self): diff --git a/pythonrewrite/bin/CryptoBoxRootActions.py b/pythonrewrite/bin/CryptoBoxRootActions.py index 77fe7f6..b92ae3c 100755 --- a/pythonrewrite/bin/CryptoBoxRootActions.py +++ b/pythonrewrite/bin/CryptoBoxRootActions.py @@ -23,6 +23,7 @@ allowedProgs = { "cryptsetup": "/sbin/cryptsetup", "mount": "/bin/mount", "umount": "/bin/umount", + "blkid": "/sbin/blkid", } @@ -185,6 +186,23 @@ def run_sfdisk(args): return True +def getFSType(device): + """get the filesystem type of a device""" + proc = subprocess.Popen( + shell = False, + stdout = subprocess.PIPE, + args = [ allowedProgs["blkid"], + "-s", "TYPE", + "-o", "value", + "-c", os.devnull, + "-w", os.devnull, + device]) + (stdout, stderr) = proc.communicate() + if proc.returncode != 0: + return None + return stdout.strip() + + def run_mount(args): """execute mount """ @@ -198,7 +216,7 @@ def run_mount(args): # check permissions for the device if not isWriteable(device, DEV_TYPES["block"]): raise "WrongArguments", "%s is not a writeable block device" % (device, ) - # check permissions for the mountpoint + ## check permissions for the mountpoint if not isWriteable(destination, DEV_TYPES["dir"]): raise "WrongArguments", "the mountpoint (%s) is not writeable" % (destination, ) # check for additional (not allowed) arguments @@ -210,18 +228,35 @@ def run_mount(args): # first overwrite the real uid, as 'mount' wants this to be zero (root) savedUID = os.getuid() os.setuid(os.geteuid()) + ## we have to change the permissions of the mounted directory - otherwise it will + ## not be writeable for the cryptobox user + ## for 'vfat' we have to do this during mount + ## for ext2/3 we have to do it afterward + ## first: get the user/group of the target + (trustUserName, trustUID, groupsOfTrustUser) = getUserInfo(savedUID) + trustGID = groupsOfTrustUser[0] + fsType = getFSType(device) + ## define arguments + if fsType == "vfat": + ## add the "uid/gid" arguments to the mount call + mount_args = [allowedProgs["mount"], + "-o", "uid=%d,gid=%d" % (trustUID, trustGID), + device, + destination] + else: + ## all other filesystem types will be handled after mount + mount_args = [allowedProgs["mount"], device, destination] # execute mount proc = subprocess.Popen( shell = False, - args = [allowedProgs["mount"], device, destination]) + args = mount_args) proc.wait() ## return in case of an error if proc.returncode != 0: return False - ## chown the mounted directory - otherwise it will not be writeable for - ## the cryptobox user (at least for the configuration partition this is - ## absolutely necessary) TODO: check if this is valid for data, too - (trustUserName, trustUID, groupsOfTrustUser) = getUserInfo(savedUID) + ## for vfat: we are done + if fsType == "vfat": return True + ## for all other filesystem types: chown the mount directory try: os.chown(destination, trustUID, groupsOfTrustUser[0]) except OSError, errMsg: diff --git a/pythonrewrite/bin/CryptoBoxSettings.py b/pythonrewrite/bin/CryptoBoxSettings.py index d8cd681..73ca9a6 100644 --- a/pythonrewrite/bin/CryptoBoxSettings.py +++ b/pythonrewrite/bin/CryptoBoxSettings.py @@ -33,6 +33,7 @@ class CryptoBoxSettings: self.__validateConfig() self.__configureLogHandler() self.__checkUnknownPreferences() + self.preparePartition() self.nameDB = self.__getNameDatabase() self.pluginConf = self.__getPluginConfig() self.userDB = self.__getUserDB() @@ -66,8 +67,8 @@ class CryptoBoxSettings: return ok - def isWriteable(self): - return os.access(self.prefs["Locations"]["SettingsDir"], os.W_OK) + def requiresPartition(self): + return bool(self.prefs["Main"]["UseConfigPartition"]) def getActivePartition(self): @@ -85,8 +86,9 @@ class CryptoBoxSettings: def mountPartition(self): - if self.isWriteable(): - self.log.warn("mountConfigPartition: configuration is already writeable - mounting anyway") + self.log.debug("trying to mount configuration partition") + if not self.requiresPartition(): + self.log.warn("mountConfigPartition: configuration partition is not required - mounting anyway") if self.getActivePartition(): self.log.warn("mountConfigPartition: configuration partition already mounted - not mounting again") return False @@ -151,6 +153,11 @@ class CryptoBoxSettings: else: return [] + + def preparePartition(self): + if self.requiresPartition() and not self.getActivePartition(): + self.mountPartition() + def __getitem__(self, key): """redirect all requests to the 'prefs' attribute""" @@ -346,7 +353,8 @@ class CryptoBoxSettings: AllowedDevices = list(min=1) DefaultVolumePrefix = string(min=1) DefaultCipher = string(default="aes-cbc-essiv:sha256") -ConfigVolumeLabel = string(min=1,default="cbox_config") +ConfigVolumeLabel = string(min=1, default="cbox_config") +UseConfigPartition = integer(min=0, max=1, default=0) [Locations] MountParentDir = directoryExists(default="/var/cache/cryptobox/mnt") diff --git a/pythonrewrite/bin/WebInterfaceDataset.py b/pythonrewrite/bin/WebInterfaceDataset.py index 8bec1e9..7f2de6c 100644 --- a/pythonrewrite/bin/WebInterfaceDataset.py +++ b/pythonrewrite/bin/WebInterfaceDataset.py @@ -14,13 +14,47 @@ class WebInterfaceDataset(dict): self.prefs = prefs self.cbox = cbox self.__setConfigValues() - self.__setCryptoBoxState() self.plugins = plugins + self.setCryptoBoxState() self.setPluginData() + self.setContainersState() - def setPluginData(self): - self.__setPluginList(self.plugins) + def setCryptoBoxState(self): + import cherrypy + self["Data.Version"] = self.cbox.VERSION + langs = self.cbox.getAvailableLanguages() + langs.sort() + for (index, lang) in enumerate(langs): + self.cbox.log.info("language loaded: %s" % lang) + self["Data.Languages.%d.name" % index] = lang + self["Data.Languages.%d.link" % index] = self.__getLanguageName(lang) + try: + self["Data.ScriptURL.Prot"] = cherrypy.request.scheme + host = cherrypy.request.headers["Host"] + self["Data.ScriptURL.Host"] = host.split(":",1)[0] + complete_url = "%s://%s" % (self["Data.ScriptURL.Prot"], self["Data.ScriptURL.Host"]) + try: + port = int(host.split(":",1)[1]) + complete_url += ":%s" % port + except (IndexError, ValueError): + if cherrypy.request.scheme == "http": + port = 80 + elif cherrypy.request.scheme == "https": + port = 443 + else: + ## unknown scheme -> port 0 + self.cbox.log.info("unknown protocol scheme used: %s" % (cherrypy.request.scheme,)) + port = 0 + self["Data.ScriptURL.Port"] = port + ## retrieve the relative address of the CGI (or the cherrypy base address) + ## remove the last part of the url and add a slash + path = "/".join(cherrypy.request.path.split("/")[:-1]) + "/" + self["Data.ScriptURL.Path"] = path + complete_url += path + self["Data.ScriptURL"] = complete_url + except AttributeError: + self["Data.ScriptURL"] = "" def setCurrentDiskState(self, device): @@ -42,6 +76,7 @@ class WebInterfaceDataset(dict): self["Data.CurrentDisk.capacity.free"] = avail self["Data.CurrentDisk.capacity.size"] = size self["Data.CurrentDisk.capacity.percent"] = percent + self["Settings.LinkAttrs.device"] = device def setContainersState(self): @@ -64,6 +99,19 @@ class WebInterfaceDataset(dict): self["Data.activeDisksCount"] = active_counter + def setPluginData(self): + for p in self.plugins: + lang_data = p.getLanguageData() + entryName = "Settings.PluginList." + p.getName() + self[entryName] = p.getName() + self[entryName + ".Link"] = lang_data.getValue("Link", p.getName()) + self[entryName + ".Rank"] = p.getRank() + self[entryName + ".RequestAuth"] = p.isAuthRequired() and "1" or "0" + self[entryName + ".Enabled"] = p.isEnabled() and "1" or "0" + for a in p.pluginCapabilities: + self[entryName + ".Types." + a] = "1" + + def __setConfigValues(self): self["Settings.TemplateDir"] = os.path.abspath(self.prefs["Locations"]["TemplateDir"]) self["Settings.LanguageDir"] = os.path.abspath(self.prefs["Locations"]["LangDir"]) @@ -74,12 +122,6 @@ class WebInterfaceDataset(dict): self["Settings.SettingsDir"] = self.prefs["Locations"]["SettingsDir"] - def __setCryptoBoxState(self): - self["Data.Version"] = self.cbox.VERSION - for lang in self.cbox.getAvailableLanguages(): - self["Data.Languages." + lang] = self.__getLanguageName(lang) - - def __getLanguageName(self, lang): try: import neo_cgi, neo_util, neo_cs @@ -91,16 +133,4 @@ class WebInterfaceDataset(dict): return hdf.getValue("Name",lang) - def __setPluginList(self, plugins): - for p in plugins: - lang_data = p.getLanguageData() - entryName = "Settings.PluginList." + p.getName() - self[entryName] = p.getName() - self[entryName + ".Link"] = lang_data.getValue("Link", p.getName()) - self[entryName + ".Rank"] = p.getRank() - self[entryName + ".RequestAuth"] = p.isAuthRequired() and "1" or "0" - self[entryName + ".Enabled"] = p.isEnabled() and "1" or "0" - for a in p.pluginCapabilities: - self[entryName + ".Types." + a] = "1" - diff --git a/pythonrewrite/bin/WebInterfaceSites.py b/pythonrewrite/bin/WebInterfaceSites.py index 0e8cde1..82405e9 100755 --- a/pythonrewrite/bin/WebInterfaceSites.py +++ b/pythonrewrite/bin/WebInterfaceSites.py @@ -17,28 +17,6 @@ except ImportError: -class WebInterfacePlugins: - - def __init__(self, log, plugins, handler_func): - for plugin in plugins.getPlugins(): - if not plugin: continue - if not plugin.isEnabled(): continue - plname = plugin.getName() - log.info("Plugin '%s' loaded" % plname) - ## this should be the "easiest" way to expose all plugins as URLs - setattr(self, plname, handler_func(plugin)) - setattr(getattr(self, plname), "exposed", True) - # TODO: check, if this really works - for now the "stream_response" feature seems to be broken - #setattr(getattr(self, plname), "stream_respones", True) - - -class IconHandler: - - def __init__(self, plugins): - self.plugins = PluginIconHandler(plugins) - self.plugins.exposed = True - - class PluginIconHandler: def __init__(self, plugins): @@ -72,11 +50,31 @@ class WebInterfaceSites: important: for _every_ "site" action (cherrypy is stateful) also take care for the plugins, as they also contain datasets """ - self.pluginList = Plugins.PluginManager(self.cbox, self.prefs["Locations"]["PluginDir"]) - self.plugins = WebInterfacePlugins(self.log, self.pluginList, self.return_plugin_action) + self.__loadPlugins() self.dataset = WebInterfaceDataset.WebInterfaceDataset(self.cbox, self.prefs, self.pluginList.getPlugins()) ## publish plugin icons - self.icons = IconHandler(self.pluginList) + self.icons = PluginIconHandler(self.pluginList) + self.icons.exposed = True + ## check, if a configuration partition has become available + self.cbox.prefs.preparePartition() + + + def __loadPlugins(self): + self.pluginList = Plugins.PluginManager(self.cbox, self.prefs["Locations"]["PluginDir"]) + for plugin in self.pluginList.getPlugins(): + if not plugin: continue + plname = plugin.getName() + if plugin.isEnabled(): + self.cbox.log.info("Plugin '%s' loaded" % plname) + ## this should be the "easiest" way to expose all plugins as URLs + setattr(self, plname, self.return_plugin_action(plugin)) + setattr(getattr(self, plname), "exposed", True) + # TODO: check, if this really works - for now the "stream_response" feature seems to be broken + #setattr(getattr(self, plname), "stream_respones", True) + else: + self.cbox.log.info("Plugin '%s' is disabled" % plname) + ## remove the plugin, if it was active before + setattr(self, plname, None) ## this is a function decorator to check authentication @@ -137,35 +135,58 @@ class WebInterfaceSites: self.__resetDataset() self.__checkEnvironment() args_orig = dict(args) + ## set web interface language try: self.__setWebLang(args["weblang"]) del args["weblang"] except KeyError: self.__setWebLang("") + ## we always read the "device" setting - otherwise volume-plugin links + ## would not work easily (see "volume_props" linking to "format_fs") + ## it will get ignored for non-volume plugins + try: + plugin.device = None + if self.__setDevice(args["device"]): + plugin.device = args["device"] + del args["device"] + except KeyError: + pass ## check the device argument of volume plugins if "volume" in plugin.pluginCapabilities: - try: - ## initialize the dataset of the selected device if necessary - if self.__setDevice(args["device"]): - plugin.device = args["device"] - self.dataset.setCurrentDiskState(plugin.device) - else: - return self.__render(self.defaultTemplate) - except KeyError: - return self.__render(self.defaultTemplate) + ## initialize the dataset of the selected device if necessary + if plugin.device: + self.dataset.setCurrentDiskState(plugin.device) else: - ## the parameter 'device' exists - we have to remove it - del args["device"] + ## invalid (or missing) device setting + return self.__render(self.defaultTemplate) + ## check if there is a "redirect" setting - this will override the return + ## value of the doAction function (e.g. useful for umount-before-format) + try: + if args["redirect"]: + override_nextTemplate = { "plugin":args["redirect"] } + if "volume" in plugin.pluginCapabilities: + override_nextTemplate["values"] = {"device":plugin.device} + del args["redirect"] + except KeyError: + override_nextTemplate = None ## call the plugin handler nextTemplate = plugin.doAction(**args) ## for 'volume' plugins: reread the dataset of the current disk ## additionally: set the default template for plugins if "volume" in plugin.pluginCapabilities: + ## maybe the state of the current volume was changed? self.dataset.setCurrentDiskState(plugin.device) if not nextTemplate: nextTemplate = { "plugin":"volume_mount", "values":{"device":plugin.device}} else: + ## maybe a non-volume plugin changed some plugin settings (e.g. plugin_manager) self.dataset.setPluginData() + ## update the container hdf-dataset (maybe a plugin changed the state of a container) + self.dataset.setContainersState() + ## default page for non-volume plugins is the disk selection if not nextTemplate: nextTemplate = { "plugin":"disks", "values":{} } + ## was a redirect requested? + if override_nextTemplate: + nextTemplate = override_nextTemplate ## if another plugins was choosen for 'nextTemplate', then do it! if isinstance(nextTemplate, types.DictType) \ and "plugin" in nextTemplate.keys() \ @@ -216,7 +237,8 @@ class WebInterfaceSites: examples are: non-https, readonly-config, ... """ - if not self.cbox.prefs.isWriteable(): + ## TODO: maybe add an option "mount"? + if self.cbox.prefs.requiresPartition() and not self.cbox.prefs.getActivePartition(): self.dataset["Data.EnvironmentWarning"] = "ReadOnlyConfig" # TODO: turn this on soon (add "not") - for now it is annoying if self.__checkHTTPS(): @@ -298,7 +320,6 @@ class WebInterfaceSites: def __setDevice(self, device): if device and re.match(u'[\w /\-]+$', device) and self.cbox.getContainer(device): self.log.debug("select device: %s" % device) - self.dataset.setCurrentDiskState(device) return True else: self.log.warn("invalid device: %s" % device) @@ -383,9 +404,6 @@ class WebInterfaceSites: yield "Couldn't read clearsilver file: %s" % cs_path return - ## update the container hdf-dataset (necessary if a plugin changed the state of a container) - self.dataset.setContainersState() - self.log.debug(self.dataset) for key in self.dataset.keys(): hdf.setValue(key,str(self.dataset[key])) diff --git a/pythonrewrite/bin/cryptobox.conf b/pythonrewrite/bin/cryptobox.conf index 95827ed..02ef334 100644 --- a/pythonrewrite/bin/cryptobox.conf +++ b/pythonrewrite/bin/cryptobox.conf @@ -4,9 +4,11 @@ # beware: .e.g "/dev/hd" grants access to _all_ harddisks AllowedDevices = /dev/loop, /dev/ubdb +# use sepepate config partition? (1=yes / 0=no) +UseConfigPartition = 1 # the default name prefix of not unnamed containers -DefaultVolumePrefix = "Data " +DefaultVolumePrefix = "Disk " # which cipher should cryptsetup-luks use? #TODO: uml does not support this module - DefaultCipher = aes-cbc-essiv:sha256 @@ -22,8 +24,7 @@ ConfigVolumeLabel = cbox_config MountParentDir = /var/cache/cryptobox/mnt # settings directory: contains name database and plugin configuration -#SettingsDir = /var/cache/cryptobox/settings -SettingsDir = . +SettingsDir = /var/cache/cryptobox/settings # where are the clearsilver templates? #TemplateDir = /usr/share/cryptobox/templates diff --git a/pythonrewrite/bin/unittests.CryptoBox.py b/pythonrewrite/bin/unittests.CryptoBox.py index e597df0..baaad3c 100755 --- a/pythonrewrite/bin/unittests.CryptoBox.py +++ b/pythonrewrite/bin/unittests.CryptoBox.py @@ -107,7 +107,7 @@ CryptoBoxRootActions = CryptoBoxRootActions def testBrokenConfigs(self): """Check various broken configurations""" - self.writeConfig("SettingsDir", "#out", filename=self.filenames["configFileBroken"]) + self.writeConfig("SettingsDir", "SettingsDir=/foo/bar", filename=self.filenames["configFileBroken"]) self.assertRaises(CBConfigError, self.CryptoBox.CryptoBoxProps,self.filenames["configFileBroken"]) self.writeConfig("Level", "Level = ho", filename=self.filenames["configFileBroken"]) self.assertRaises(CBConfigError, self.CryptoBox.CryptoBoxProps,self.filenames["configFileBroken"])