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)
This commit is contained in:
lars 2006-11-06 06:16:27 +00:00
parent 4df227a99d
commit 0c23c5e2c3
8 changed files with 225 additions and 94 deletions

View file

@ -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):
@ -248,6 +249,14 @@ class CryptoBoxProps(CryptoBox):
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):
'''reads all files in path LangDir and returns a list of
basenames from existing hdf files, that should are all available
@ -261,6 +270,7 @@ class CryptoBoxProps(CryptoBox):
if __name__ == "__main__":
cb = CryptoBoxProps()

View file

@ -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
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):

View file

@ -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:

View file

@ -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
@ -152,6 +154,11 @@ class CryptoBoxSettings:
return []
def preparePartition(self):
if self.requiresPartition() and not self.getActivePartition():
self.mountPartition()
def __getitem__(self, key):
"""redirect all requests to the 'prefs' attribute"""
return self.prefs[key]
@ -347,6 +354,7 @@ AllowedDevices = list(min=1)
DefaultVolumePrefix = string(min=1)
DefaultCipher = string(default="aes-cbc-essiv:sha256")
ConfigVolumeLabel = string(min=1, default="cbox_config")
UseConfigPartition = integer(min=0, max=1, default=0)
[Locations]
MountParentDir = directoryExists(default="/var/cache/cryptobox/mnt")

View file

@ -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"

View file

@ -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("")
## check the device argument of volume plugins
if "volume" in plugin.pluginCapabilities:
## 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:
## initialize the dataset of the selected device if necessary
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:
## initialize the dataset of the selected device if necessary
if plugin.device:
self.dataset.setCurrentDiskState(plugin.device)
else:
## 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:
return self.__render(self.defaultTemplate)
else:
## the parameter 'device' exists - we have to remove it
del args["device"]
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]))

View file

@ -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

View file

@ -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"])