| 1 | |
|---|
| 2 | |
|---|
| 3 | |
|---|
| 4 | |
|---|
| 5 | |
|---|
| 6 | |
|---|
| 7 | |
|---|
| 8 | |
|---|
| 9 | |
|---|
| 10 | |
|---|
| 11 | |
|---|
| 12 | |
|---|
| 13 | |
|---|
| 14 | |
|---|
| 15 | |
|---|
| 16 | |
|---|
| 17 | |
|---|
| 18 | |
|---|
| 19 | |
|---|
| 20 | |
|---|
| 21 | """Manage the configuration of a CryptoBox |
|---|
| 22 | """ |
|---|
| 23 | |
|---|
| 24 | __revision__ = "$Id$" |
|---|
| 25 | |
|---|
| 26 | from cryptobox.core.exceptions import * |
|---|
| 27 | import logging |
|---|
| 28 | import subprocess |
|---|
| 29 | import os |
|---|
| 30 | import configobj, validate |
|---|
| 31 | import syslog |
|---|
| 32 | |
|---|
| 33 | |
|---|
| 34 | CONF_LOCATIONS = [ |
|---|
| 35 | "./cryptobox.conf", |
|---|
| 36 | "~/.cryptobox.conf", |
|---|
| 37 | "/etc/cryptobox-server/cryptobox.conf"] |
|---|
| 38 | |
|---|
| 39 | VOLUMESDB_FILE = "cryptobox_volumes.db" |
|---|
| 40 | PLUGINCONF_FILE = "cryptobox_plugins.conf" |
|---|
| 41 | USERDB_FILE = "cryptobox_users.db" |
|---|
| 42 | |
|---|
| 43 | |
|---|
| 44 | class CryptoBoxSettings: |
|---|
| 45 | """Manage the various configuration files of the CryptoBox |
|---|
| 46 | """ |
|---|
| 47 | |
|---|
| 48 | def __init__(self, config_file=None): |
|---|
| 49 | self.__is_initialized = False |
|---|
| 50 | self.log = logging.getLogger("CryptoBox") |
|---|
| 51 | config_file = self.__get_config_filename(config_file) |
|---|
| 52 | self.log.info("loading config file: %s" % config_file) |
|---|
| 53 | self.prefs = self.__get_preferences(config_file) |
|---|
| 54 | if not "PluginSettings" in self.prefs: |
|---|
| 55 | self.prefs["PluginSettings"] = {} |
|---|
| 56 | self.__validate_config() |
|---|
| 57 | self.__configure_log_handler() |
|---|
| 58 | self.__check_unknown_preferences() |
|---|
| 59 | self.prepare_partition() |
|---|
| 60 | self.volumes_db = self.__get_volumes_database() |
|---|
| 61 | self.plugin_conf = self.__get_plugin_config() |
|---|
| 62 | self.user_db = self.__get_user_db() |
|---|
| 63 | self.misc_files = [] |
|---|
| 64 | self.reload_misc_files() |
|---|
| 65 | self.__is_initialized = True |
|---|
| 66 | |
|---|
| 67 | |
|---|
| 68 | def reload_misc_files(self): |
|---|
| 69 | """Call this method after creating or removing a 'misc' configuration file |
|---|
| 70 | """ |
|---|
| 71 | self.misc_files = self.__get_misc_files() |
|---|
| 72 | |
|---|
| 73 | |
|---|
| 74 | def write(self): |
|---|
| 75 | """ |
|---|
| 76 | write all local setting files including the content of the "misc" subdirectory |
|---|
| 77 | """ |
|---|
| 78 | status = True |
|---|
| 79 | try: |
|---|
| 80 | self.volumes_db.write() |
|---|
| 81 | except IOError: |
|---|
| 82 | self.log.warn("Could not save the volume database") |
|---|
| 83 | status = False |
|---|
| 84 | try: |
|---|
| 85 | self.plugin_conf.write() |
|---|
| 86 | except IOError: |
|---|
| 87 | self.log.warn("Could not save the plugin configuration") |
|---|
| 88 | status = False |
|---|
| 89 | try: |
|---|
| 90 | self.user_db.write() |
|---|
| 91 | except IOError: |
|---|
| 92 | self.log.warn("Could not save the user database") |
|---|
| 93 | status = False |
|---|
| 94 | for misc_file in self.misc_files: |
|---|
| 95 | if not misc_file.save(): |
|---|
| 96 | self.log.warn("Could not save a misc setting file (%s)" % misc_file.filename) |
|---|
| 97 | status = False |
|---|
| 98 | return status |
|---|
| 99 | |
|---|
| 100 | |
|---|
| 101 | def get_misc_config_filename(self, name): |
|---|
| 102 | """Return an absolute filename for a given filename 'name' |
|---|
| 103 | |
|---|
| 104 | 'name' should not contain slashes (no directory part!) |
|---|
| 105 | """ |
|---|
| 106 | return os.path.join(self.prefs["Locations"]["SettingsDir"], "misc", name) |
|---|
| 107 | |
|---|
| 108 | |
|---|
| 109 | def create_misc_config_file(self, name, content): |
|---|
| 110 | """Create a new configuration file in the 'settings' directory |
|---|
| 111 | |
|---|
| 112 | "name" should be the basename (without a directory) |
|---|
| 113 | "content" will be directly written to the file |
|---|
| 114 | this method may throw an IOException |
|---|
| 115 | """ |
|---|
| 116 | misc_conf_file = self.get_misc_config_filename(name) |
|---|
| 117 | misc_conf_dir = os.path.dirname(misc_conf_file) |
|---|
| 118 | if not os.path.isdir(misc_conf_dir): |
|---|
| 119 | try: |
|---|
| 120 | os.mkdir(misc_conf_dir) |
|---|
| 121 | except OSError, err_msg: |
|---|
| 122 | |
|---|
| 123 | raise IOError, err_msg |
|---|
| 124 | cfile = open(misc_conf_file, "w") |
|---|
| 125 | try: |
|---|
| 126 | cfile.write(content) |
|---|
| 127 | except IOError: |
|---|
| 128 | cfile.close() |
|---|
| 129 | raise |
|---|
| 130 | cfile.close() |
|---|
| 131 | |
|---|
| 132 | self.reload_misc_files() |
|---|
| 133 | |
|---|
| 134 | |
|---|
| 135 | def requires_partition(self): |
|---|
| 136 | return bool(self.prefs["Main"]["UseConfigPartition"]) |
|---|
| 137 | |
|---|
| 138 | |
|---|
| 139 | def get_active_partition(self): |
|---|
| 140 | """Return the currently active cnfiguration partition. |
|---|
| 141 | """ |
|---|
| 142 | settings_dir = self.prefs["Locations"]["SettingsDir"] |
|---|
| 143 | if not os.path.ismount(settings_dir): |
|---|
| 144 | return None |
|---|
| 145 | for line in file("/proc/mounts"): |
|---|
| 146 | fields = line.split(" ") |
|---|
| 147 | mount_dir = fields[1] |
|---|
| 148 | fs_type = fields[2] |
|---|
| 149 | if fs_type == "tmpfs": |
|---|
| 150 | |
|---|
| 151 | continue |
|---|
| 152 | try: |
|---|
| 153 | if os.path.samefile(mount_dir, settings_dir): |
|---|
| 154 | return fields[0] |
|---|
| 155 | except OSError: |
|---|
| 156 | pass |
|---|
| 157 | |
|---|
| 158 | return None |
|---|
| 159 | |
|---|
| 160 | |
|---|
| 161 | def mount_partition(self): |
|---|
| 162 | """Mount a config partition. |
|---|
| 163 | """ |
|---|
| 164 | self.log.debug("trying to mount configuration partition") |
|---|
| 165 | if not self.requires_partition(): |
|---|
| 166 | self.log.warn("mountConfigPartition: configuration partition is " |
|---|
| 167 | + "not required - mounting anyway") |
|---|
| 168 | if self.get_active_partition(): |
|---|
| 169 | self.log.warn("mountConfigPartition: configuration partition already " |
|---|
| 170 | + "mounted - not mounting again") |
|---|
| 171 | return False |
|---|
| 172 | conf_partitions = self.get_available_partitions() |
|---|
| 173 | mount_dir = self.prefs["Locations"]["SettingsDir"] |
|---|
| 174 | if not conf_partitions: |
|---|
| 175 | |
|---|
| 176 | if os.path.ismount(mount_dir): |
|---|
| 177 | self.log.info("A ramdisk seems to be already mounted as a config " \ |
|---|
| 178 | + "partition - doing nothing ...") |
|---|
| 179 | |
|---|
| 180 | return True |
|---|
| 181 | self.log.warn("no configuration partition found - you have to create " |
|---|
| 182 | + "it first") |
|---|
| 183 | |
|---|
| 184 | |
|---|
| 185 | |
|---|
| 186 | proc = subprocess.Popen( |
|---|
| 187 | shell = False, |
|---|
| 188 | stdout = subprocess.PIPE, |
|---|
| 189 | stderr = subprocess.PIPE, |
|---|
| 190 | args = [ |
|---|
| 191 | self.prefs["Programs"]["super"], |
|---|
| 192 | self.prefs["Programs"]["CryptoBoxRootActions"], |
|---|
| 193 | "mount", |
|---|
| 194 | "_tmpfs_", |
|---|
| 195 | mount_dir ]) |
|---|
| 196 | (stdout, stderr) = proc.communicate() |
|---|
| 197 | if proc.returncode != 0: |
|---|
| 198 | self.log.error("Failed to mount a ramdisk for storing settings: %s" \ |
|---|
| 199 | % stderr) |
|---|
| 200 | return False |
|---|
| 201 | self.log.info("Ramdisk (tmpfs) mounted as config partition ...") |
|---|
| 202 | else: |
|---|
| 203 | partition = conf_partitions[0] |
|---|
| 204 | |
|---|
| 205 | if os.path.ismount(mount_dir): |
|---|
| 206 | self.umount_partition() |
|---|
| 207 | proc = subprocess.Popen( |
|---|
| 208 | shell = False, |
|---|
| 209 | stdout = subprocess.PIPE, |
|---|
| 210 | stderr = subprocess.PIPE, |
|---|
| 211 | args = [ |
|---|
| 212 | self.prefs["Programs"]["super"], |
|---|
| 213 | self.prefs["Programs"]["CryptoBoxRootActions"], |
|---|
| 214 | "mount", |
|---|
| 215 | partition, |
|---|
| 216 | mount_dir ]) |
|---|
| 217 | (stdout, stderr) = proc.communicate() |
|---|
| 218 | if proc.returncode != 0: |
|---|
| 219 | self.log.error("Failed to mount the configuration partition (%s): %s" % \ |
|---|
| 220 | (partition, stderr)) |
|---|
| 221 | return False |
|---|
| 222 | self.log.info("configuration partition mounted: %s" % partition) |
|---|
| 223 | |
|---|
| 224 | if self.__is_initialized: |
|---|
| 225 | self.write() |
|---|
| 226 | return True |
|---|
| 227 | |
|---|
| 228 | |
|---|
| 229 | def umount_partition(self): |
|---|
| 230 | """Umount the currently active configuration partition. |
|---|
| 231 | """ |
|---|
| 232 | mount_dir = self.prefs["Locations"]["SettingsDir"] |
|---|
| 233 | if not os.path.ismount(mount_dir): |
|---|
| 234 | self.log.warn("umountConfigPartition: no configuration partition mounted") |
|---|
| 235 | return False |
|---|
| 236 | self.reload_misc_files() |
|---|
| 237 | proc = subprocess.Popen( |
|---|
| 238 | shell = False, |
|---|
| 239 | stdout = subprocess.PIPE, |
|---|
| 240 | stderr = subprocess.PIPE, |
|---|
| 241 | args = [ |
|---|
| 242 | self.prefs["Programs"]["super"], |
|---|
| 243 | self.prefs["Programs"]["CryptoBoxRootActions"], |
|---|
| 244 | "umount", |
|---|
| 245 | mount_dir ]) |
|---|
| 246 | (stdout, stderr) = proc.communicate() |
|---|
| 247 | if proc.returncode != 0: |
|---|
| 248 | self.log.error("Failed to unmount the configuration partition: %s" % stderr) |
|---|
| 249 | return False |
|---|
| 250 | self.log.info("configuration partition unmounted") |
|---|
| 251 | return True |
|---|
| 252 | |
|---|
| 253 | |
|---|
| 254 | def get_available_partitions(self): |
|---|
| 255 | """returns a sequence of found config partitions""" |
|---|
| 256 | self.log.debug("Retrieving available configuration partitions ...") |
|---|
| 257 | proc = subprocess.Popen( |
|---|
| 258 | shell = False, |
|---|
| 259 | stdout = subprocess.PIPE, |
|---|
| 260 | stderr = subprocess.PIPE, |
|---|
| 261 | args = [ |
|---|
| 262 | self.prefs["Programs"]["blkid"], |
|---|
| 263 | "-c", os.path.devnull, |
|---|
| 264 | "-t", "LABEL=%s" % self.prefs["Main"]["ConfigVolumeLabel"] ]) |
|---|
| 265 | (output, error) = proc.communicate() |
|---|
| 266 | if proc.returncode == 2: |
|---|
| 267 | self.log.info("No configuration partitions found") |
|---|
| 268 | return [] |
|---|
| 269 | elif proc.returncode == 4: |
|---|
| 270 | self.log.warn("Failed to call 'blkid' for unknown reasons.") |
|---|
| 271 | return [] |
|---|
| 272 | elif proc.returncode == 0: |
|---|
| 273 | if output: |
|---|
| 274 | return [e.strip().split(":", 1)[0] for e in output.splitlines()] |
|---|
| 275 | else: |
|---|
| 276 | return [] |
|---|
| 277 | else: |
|---|
| 278 | self.log.warn("Unknown exit code of 'blkid': %d - %s" \ |
|---|
| 279 | % (proc.returncode, error)) |
|---|
| 280 | |
|---|
| 281 | |
|---|
| 282 | def prepare_partition(self): |
|---|
| 283 | """Mount a config partition if necessary. |
|---|
| 284 | """ |
|---|
| 285 | if self.requires_partition() and not self.get_active_partition(): |
|---|
| 286 | self.mount_partition() |
|---|
| 287 | |
|---|
| 288 | |
|---|
| 289 | def __getitem__(self, key): |
|---|
| 290 | """redirect all requests to the 'prefs' attribute""" |
|---|
| 291 | return self.prefs[key] |
|---|
| 292 | |
|---|
| 293 | |
|---|
| 294 | def __get_preferences(self, config_file): |
|---|
| 295 | """Load the CryptoBox configuration. |
|---|
| 296 | """ |
|---|
| 297 | import StringIO |
|---|
| 298 | config_rules = StringIO.StringIO(self.validation_spec) |
|---|
| 299 | try: |
|---|
| 300 | prefs = configobj.ConfigObj(config_file, configspec=config_rules) |
|---|
| 301 | if prefs: |
|---|
| 302 | self.log.info("found config: %s" % prefs.items()) |
|---|
| 303 | else: |
|---|
| 304 | raise CBConfigUnavailableError( |
|---|
| 305 | "failed to load the config file: %s" % config_file) |
|---|
| 306 | except IOError: |
|---|
| 307 | raise CBConfigUnavailableError( |
|---|
| 308 | "unable to open the config file: %s" % config_file) |
|---|
| 309 | return prefs |
|---|
| 310 | |
|---|
| 311 | |
|---|
| 312 | def __validate_config(self): |
|---|
| 313 | """Check the configuration settings and cast value types. |
|---|
| 314 | """ |
|---|
| 315 | result = self.prefs.validate(CryptoBoxSettingsValidator(), preserve_errors=True) |
|---|
| 316 | error_list = configobj.flatten_errors(self.prefs, result) |
|---|
| 317 | if not error_list: |
|---|
| 318 | return |
|---|
| 319 | error_msgs = [] |
|---|
| 320 | for sections, key, text in error_list: |
|---|
| 321 | section_name = "->".join(sections) |
|---|
| 322 | if not text: |
|---|
| 323 | error_msg = "undefined configuration value (%s) in section '%s'" % \ |
|---|
| 324 | (key, section_name) |
|---|
| 325 | else: |
|---|
| 326 | error_msg = "invalid configuration value (%s) in section '%s': %s" % \ |
|---|
| 327 | (key, section_name, text) |
|---|
| 328 | error_msgs.append(error_msg) |
|---|
| 329 | raise CBConfigError, "\n".join(error_msgs) |
|---|
| 330 | |
|---|
| 331 | |
|---|
| 332 | def __check_unknown_preferences(self): |
|---|
| 333 | """Check the configuration file for unknown settings to avoid spelling mistakes. |
|---|
| 334 | """ |
|---|
| 335 | import StringIO |
|---|
| 336 | config_rules = configobj.ConfigObj(StringIO.StringIO(self.validation_spec), |
|---|
| 337 | list_values=False) |
|---|
| 338 | self.__recursive_section_check("", self.prefs, config_rules) |
|---|
| 339 | |
|---|
| 340 | |
|---|
| 341 | def __recursive_section_check(self, section_path, section_config, section_rules): |
|---|
| 342 | """should be called by '__check_unknown_preferences' for every section |
|---|
| 343 | sends a warning message to the logger for every undefined (see validation_spec) |
|---|
| 344 | configuration setting |
|---|
| 345 | """ |
|---|
| 346 | for section in section_config.keys(): |
|---|
| 347 | element_path = section_path + section |
|---|
| 348 | if section in section_rules.keys(): |
|---|
| 349 | if isinstance(section_config[section], configobj.Section): |
|---|
| 350 | if isinstance(section_rules[section], configobj.Section): |
|---|
| 351 | self.__recursive_section_check(element_path + "->", |
|---|
| 352 | section_config[section], section_rules[section]) |
|---|
| 353 | else: |
|---|
| 354 | self.log.warn("configuration setting should be a value " |
|---|
| 355 | + "instead of a section name: %s" % element_path) |
|---|
| 356 | else: |
|---|
| 357 | if not isinstance(section_rules[section], configobj.Section): |
|---|
| 358 | pass |
|---|
| 359 | else: |
|---|
| 360 | self.log.warn("configuration setting should be a section " |
|---|
| 361 | + "name instead of a value: %s" % element_path) |
|---|
| 362 | elif element_path.startswith("PluginSettings->"): |
|---|
| 363 | |
|---|
| 364 | pass |
|---|
| 365 | else: |
|---|
| 366 | self.log.warn("unknown configuration setting: %s" % element_path) |
|---|
| 367 | |
|---|
| 368 | |
|---|
| 369 | def __get_plugin_config(self): |
|---|
| 370 | """Load the plugin configuration file if it exists. |
|---|
| 371 | """ |
|---|
| 372 | import StringIO |
|---|
| 373 | plugin_rules = StringIO.StringIO(self.pluginValidationSpec) |
|---|
| 374 | try: |
|---|
| 375 | try: |
|---|
| 376 | plugin_conf_file = os.path.join( |
|---|
| 377 | self.prefs["Locations"]["SettingsDir"], PLUGINCONF_FILE) |
|---|
| 378 | except KeyError: |
|---|
| 379 | raise CBConfigUndefinedError("Locations", "SettingsDir") |
|---|
| 380 | except SyntaxError: |
|---|
| 381 | raise CBConfigInvalidValueError("Locations", "SettingsDir", plugin_conf_file, |
|---|
| 382 | "failed to interprete the filename of the plugin config file " |
|---|
| 383 | + "correctly (%s)" % plugin_conf_file) |
|---|
| 384 | |
|---|
| 385 | if os.path.exists(plugin_conf_file): |
|---|
| 386 | plugin_conf = configobj.ConfigObj(plugin_conf_file, configspec=plugin_rules) |
|---|
| 387 | else: |
|---|
| 388 | try: |
|---|
| 389 | plugin_conf = configobj.ConfigObj(plugin_conf_file, |
|---|
| 390 | configspec=plugin_rules, create_empty=True) |
|---|
| 391 | except IOError: |
|---|
| 392 | plugin_conf = configobj.ConfigObj(configspec=plugin_rules) |
|---|
| 393 | plugin_conf.filename = plugin_conf_file |
|---|
| 394 | |
|---|
| 395 | plugin_conf.validate(validate.Validator()) |
|---|
| 396 | return plugin_conf |
|---|
| 397 | |
|---|
| 398 | |
|---|
| 399 | def __get_volumes_database(self): |
|---|
| 400 | """Load the volume database file if it exists. |
|---|
| 401 | """ |
|---|
| 402 | |
|---|
| 403 | try: |
|---|
| 404 | try: |
|---|
| 405 | conf_file = os.path.join( |
|---|
| 406 | self.prefs["Locations"]["SettingsDir"], VOLUMESDB_FILE) |
|---|
| 407 | except KeyError: |
|---|
| 408 | raise CBConfigUndefinedError("Locations", "SettingsDir") |
|---|
| 409 | except SyntaxError: |
|---|
| 410 | raise CBConfigInvalidValueError("Locations", "SettingsDir", conf_file, |
|---|
| 411 | "failed to interprete the filename of the volume database " |
|---|
| 412 | + "correctly (%s)" % conf_file) |
|---|
| 413 | |
|---|
| 414 | if os.path.exists(conf_file): |
|---|
| 415 | conf = configobj.ConfigObj(conf_file) |
|---|
| 416 | else: |
|---|
| 417 | try: |
|---|
| 418 | conf = configobj.ConfigObj(conf_file, create_empty=True) |
|---|
| 419 | except IOError: |
|---|
| 420 | conf = configobj.ConfigObj() |
|---|
| 421 | conf.filename = conf_file |
|---|
| 422 | return conf |
|---|
| 423 | |
|---|
| 424 | |
|---|
| 425 | def __get_user_db(self): |
|---|
| 426 | """Load the user database file if it exists. |
|---|
| 427 | """ |
|---|
| 428 | import StringIO, sha |
|---|
| 429 | user_db_rules = StringIO.StringIO(self.userDatabaseSpec) |
|---|
| 430 | try: |
|---|
| 431 | try: |
|---|
| 432 | user_db_file = os.path.join( |
|---|
| 433 | self.prefs["Locations"]["SettingsDir"], USERDB_FILE) |
|---|
| 434 | except KeyError: |
|---|
| 435 | raise CBConfigUndefinedError("Locations", "SettingsDir") |
|---|
| 436 | except SyntaxError: |
|---|
| 437 | raise CBConfigInvalidValueError("Locations", "SettingsDir", user_db_file, |
|---|
| 438 | "failed to interprete the filename of the users database file " |
|---|
| 439 | + "correctly (%s)" % user_db_file) |
|---|
| 440 | |
|---|
| 441 | if os.path.exists(user_db_file): |
|---|
| 442 | user_db = configobj.ConfigObj(user_db_file, configspec=user_db_rules) |
|---|
| 443 | else: |
|---|
| 444 | try: |
|---|
| 445 | user_db = configobj.ConfigObj(user_db_file, |
|---|
| 446 | configspec=user_db_rules, create_empty=True) |
|---|
| 447 | except IOError: |
|---|
| 448 | user_db = configobj.ConfigObj(configspec=user_db_rules) |
|---|
| 449 | user_db.filename = user_db_file |
|---|
| 450 | |
|---|
| 451 | user_db.validate(validate.Validator()) |
|---|
| 452 | |
|---|
| 453 | user_db.get_digest = lambda password: sha.new(password).hexdigest() |
|---|
| 454 | return user_db |
|---|
| 455 | |
|---|
| 456 | |
|---|
| 457 | def __get_misc_files(self): |
|---|
| 458 | """Load miscelleanous configuration files. |
|---|
| 459 | |
|---|
| 460 | e.g.: an ssl certificate, ... |
|---|
| 461 | """ |
|---|
| 462 | misc_dir = os.path.join(self.prefs["Locations"]["SettingsDir"], "misc") |
|---|
| 463 | if (not os.path.isdir(misc_dir)) or (not os.access(misc_dir, os.X_OK)): |
|---|
| 464 | return [] |
|---|
| 465 | misc_files = [] |
|---|
| 466 | for root, dirs, files in os.walk(misc_dir): |
|---|
| 467 | misc_files.extend([os.path.join(root, e) for e in files]) |
|---|
| 468 | return [MiscConfigFile(os.path.join(misc_dir, f), self.log) for f in misc_files] |
|---|
| 469 | |
|---|
| 470 | |
|---|
| 471 | def __get_config_filename(self, config_file): |
|---|
| 472 | """Search for the configuration file. |
|---|
| 473 | """ |
|---|
| 474 | import types |
|---|
| 475 | if config_file is None: |
|---|
| 476 | |
|---|
| 477 | conf_file_list = [os.path.expanduser(f) |
|---|
| 478 | for f in CONF_LOCATIONS |
|---|
| 479 | if os.path.exists(os.path.expanduser(f))] |
|---|
| 480 | if not conf_file_list: |
|---|
| 481 | |
|---|
| 482 | raise CBConfigUnavailableError() |
|---|
| 483 | config_file = conf_file_list[0] |
|---|
| 484 | else: |
|---|
| 485 | |
|---|
| 486 | if type(config_file) != types.StringType: |
|---|
| 487 | raise CBConfigUnavailableError( |
|---|
| 488 | "invalid config file specified: %s" % config_file) |
|---|
| 489 | if not os.path.exists(config_file): |
|---|
| 490 | raise CBConfigUnavailableError( |
|---|
| 491 | "could not find the specified configuration file (%s)" % config_file) |
|---|
| 492 | return config_file |
|---|
| 493 | |
|---|
| 494 | |
|---|
| 495 | def __configure_log_handler(self): |
|---|
| 496 | """Configure the log handler of the CryptoBox according to the config. |
|---|
| 497 | """ |
|---|
| 498 | log_level = self.prefs["Log"]["Level"].upper() |
|---|
| 499 | log_level_avail = ["DEBUG", "INFO", "WARN", "ERROR"] |
|---|
| 500 | if not log_level in log_level_avail: |
|---|
| 501 | raise CBConfigInvalidValueError("Log", "Level", log_level, |
|---|
| 502 | "invalid log level: only %s are allowed" % str(log_level_avail)) |
|---|
| 503 | log_destination = self.prefs["Log"]["Destination"].lower() |
|---|
| 504 | |
|---|
| 505 | log_dest_avail = ['file', 'syslog'] |
|---|
| 506 | if not log_destination in log_dest_avail: |
|---|
| 507 | raise CBConfigInvalidValueError("Log", "Destination", log_destination, |
|---|
| 508 | "invalid log destination: only %s are allowed" % str(log_dest_avail)) |
|---|
| 509 | if log_destination == 'file': |
|---|
| 510 | try: |
|---|
| 511 | log_handler = logging.FileHandler(self.prefs["Log"]["Details"]) |
|---|
| 512 | except IOError: |
|---|
| 513 | raise CBEnvironmentError("could not write to log file (%s)" % \ |
|---|
| 514 | self.prefs["Log"]["Details"]) |
|---|
| 515 | log_handler.setFormatter( |
|---|
| 516 | logging.Formatter('%(asctime)s %(levelname)s: %(message)s')) |
|---|
| 517 | elif log_destination == 'syslog': |
|---|
| 518 | log_facility = self.prefs["Log"]["Details"].upper() |
|---|
| 519 | log_facil_avail = ['KERN', 'USER', 'MAIL', 'DAEMON', 'AUTH', 'SYSLOG', |
|---|
| 520 | 'LPR', 'NEWS', 'UUCP', 'CRON', 'AUTHPRIV', 'LOCAL0', 'LOCAL1', |
|---|
| 521 | 'LOCAL2', 'LOCAL3', 'LOCAL4', 'LOCAL5', 'LOCAL6', 'LOCAL7'] |
|---|
| 522 | if not log_facility in log_facil_avail: |
|---|
| 523 | raise CBConfigInvalidValueError("Log", "Details", log_facility, |
|---|
| 524 | "invalid log details for 'syslog': only %s are allowed" % \ |
|---|
| 525 | str(log_facil_avail)) |
|---|
| 526 | |
|---|
| 527 | log_handler = LocalSysLogHandler("CryptoBox", |
|---|
| 528 | getattr(syslog, 'LOG_%s' % log_facility)) |
|---|
| 529 | log_handler.setFormatter( |
|---|
| 530 | logging.Formatter('%(asctime)s CryptoBox %(levelname)s: %(message)s')) |
|---|
| 531 | else: |
|---|
| 532 | |
|---|
| 533 | |
|---|
| 534 | raise CBConfigInvalidValueError("Log", "Destination", log_destination, |
|---|
| 535 | "invalid log destination: only %s are allowed" % str(log_dest_avail)) |
|---|
| 536 | cbox_log = logging.getLogger("CryptoBox") |
|---|
| 537 | |
|---|
| 538 | cbox_log.handlers = [] |
|---|
| 539 | |
|---|
| 540 | cbox_log.addHandler(log_handler) |
|---|
| 541 | |
|---|
| 542 | cbox_log.propagate = False |
|---|
| 543 | |
|---|
| 544 | cbox_log.setLevel(getattr(logging, log_level)) |
|---|
| 545 | |
|---|
| 546 | |
|---|
| 547 | |
|---|
| 548 | validation_spec = """ |
|---|
| 549 | [Main] |
|---|
| 550 | AllowedDevices = list(min=1) |
|---|
| 551 | DefaultVolumePrefix = string(min=1) |
|---|
| 552 | DefaultCipher = string(default="aes-cbc-essiv:sha256") |
|---|
| 553 | ConfigVolumeLabel = string(min=1, default="cbox_config") |
|---|
| 554 | UseConfigPartition = integer(min=0, max=1, default=0) |
|---|
| 555 | DisabledPlugins = list(default=list()) |
|---|
| 556 | |
|---|
| 557 | [Locations] |
|---|
| 558 | MountParentDir = directoryExists(default="/var/cache/cryptobox-server/mnt") |
|---|
| 559 | SettingsDir = directoryExists(default="/var/cache/cryptobox-server/settings") |
|---|
| 560 | TemplateDir = directoryExists(default="/usr/share/cryptobox-server/template") |
|---|
| 561 | DocDir = directoryExists(default="/usr/share/doc/cryptobox-server/www-data") |
|---|
| 562 | PluginDir = listOfExistingDirectories(default=list("/usr/share/cryptobox-server/plugins")) |
|---|
| 563 | EventDir = string(default="/etc/cryptobox-server/events.d") |
|---|
| 564 | |
|---|
| 565 | [Log] |
|---|
| 566 | Level = option("debug", "info", "warn", "error", default="warn") |
|---|
| 567 | Destination = option("file", "syslog", default="file") |
|---|
| 568 | Details = string(min=1, default="/var/log/cryptobox-server/cryptobox.log") |
|---|
| 569 | |
|---|
| 570 | [WebSettings] |
|---|
| 571 | Stylesheet = string(min=1) |
|---|
| 572 | Languages = list(min=1,default=list("en")) |
|---|
| 573 | |
|---|
| 574 | [Programs] |
|---|
| 575 | cryptsetup = fileExecutable(default="/sbin/cryptsetup") |
|---|
| 576 | mkfs = fileExecutable(default="/sbin/mkfs") |
|---|
| 577 | nice = fileExecutable(default="/usr/bin/nice") |
|---|
| 578 | blkid = fileExecutable(default="/sbin/blkid") |
|---|
| 579 | blockdev = fileExecutable(default="/sbin/blockdev") |
|---|
| 580 | mount = fileExecutable(default="/bin/mount") |
|---|
| 581 | umount = fileExecutable(default="/bin/umount") |
|---|
| 582 | super = fileExecutable(default="/usr/bin/super") |
|---|
| 583 | # this is the "program" name as defined in /etc/super.tab |
|---|
| 584 | CryptoBoxRootActions = string(min=1) |
|---|
| 585 | |
|---|
| 586 | [PluginSettings] |
|---|
| 587 | [[__many__]] |
|---|
| 588 | """ |
|---|
| 589 | |
|---|
| 590 | pluginValidationSpec = """ |
|---|
| 591 | [__many__] |
|---|
| 592 | visibility = boolean(default=None) |
|---|
| 593 | requestAuth = boolean(default=None) |
|---|
| 594 | rank = integer(default=None) |
|---|
| 595 | """ |
|---|
| 596 | |
|---|
| 597 | userDatabaseSpec = """ |
|---|
| 598 | [admins] |
|---|
| 599 | admin = string(default=d033e22ae348aeb5660fc2140aec35850c4da997) |
|---|
| 600 | """ |
|---|
| 601 | |
|---|
| 602 | |
|---|
| 603 | class CryptoBoxSettingsValidator(validate.Validator): |
|---|
| 604 | """Some custom configuration check functions. |
|---|
| 605 | """ |
|---|
| 606 | |
|---|
| 607 | def __init__(self): |
|---|
| 608 | validate.Validator.__init__(self) |
|---|
| 609 | self.functions["directoryExists"] = self.check_directory_exists |
|---|
| 610 | self.functions["fileExecutable"] = self.check_file_executable |
|---|
| 611 | self.functions["fileWriteable"] = self.check_file_writeable |
|---|
| 612 | self.functions["listOfExistingDirectories"] = self.check_existing_directories |
|---|
| 613 | |
|---|
| 614 | |
|---|
| 615 | def check_directory_exists(self, value): |
|---|
| 616 | """Is the directory accessible? |
|---|
| 617 | """ |
|---|
| 618 | dir_path = os.path.abspath(value) |
|---|
| 619 | if not os.path.isdir(dir_path): |
|---|
| 620 | raise validate.VdtValueError("%s (not found)" % value) |
|---|
| 621 | if not os.access(dir_path, os.X_OK): |
|---|
| 622 | raise validate.VdtValueError("%s (access denied)" % value) |
|---|
| 623 | return dir_path |
|---|
| 624 | |
|---|
| 625 | |
|---|
| 626 | def check_file_executable(self, value): |
|---|
| 627 | """Is the file executable? |
|---|
| 628 | """ |
|---|
| 629 | file_path = os.path.abspath(value) |
|---|
| 630 | if not os.path.isfile(file_path): |
|---|
| 631 | raise validate.VdtValueError("%s (not found)" % value) |
|---|
| 632 | if not os.access(file_path, os.X_OK): |
|---|
| 633 | raise validate.VdtValueError("%s (access denied)" % value) |
|---|
| 634 | return file_path |
|---|
| 635 | |
|---|
| 636 | |
|---|
| 637 | def check_file_writeable(self, value): |
|---|
| 638 | """Is the file writeable? |
|---|
| 639 | """ |
|---|
| 640 | file_path = os.path.abspath(value) |
|---|
| 641 | if os.path.isfile(file_path): |
|---|
| 642 | if not os.access(file_path, os.W_OK): |
|---|
| 643 | raise validate.VdtValueError("%s (not found)" % value) |
|---|
| 644 | else: |
|---|
| 645 | parent_dir = os.path.dirname(file_path) |
|---|
| 646 | if os.path.isdir(parent_dir) and os.access(parent_dir, os.W_OK): |
|---|
| 647 | return file_path |
|---|
| 648 | raise validate.VdtValueError("%s (directory does not exist)" % value) |
|---|
| 649 | return file_path |
|---|
| 650 | |
|---|
| 651 | |
|---|
| 652 | def check_existing_directories(self, value): |
|---|
| 653 | """Are these directories accessible? |
|---|
| 654 | """ |
|---|
| 655 | if not value: |
|---|
| 656 | raise validate.VdtValueError("no plugin directory specified") |
|---|
| 657 | if not isinstance(value, list): |
|---|
| 658 | value = [value] |
|---|
| 659 | result = [] |
|---|
| 660 | for one_dir in value: |
|---|
| 661 | dir_path = os.path.abspath(one_dir) |
|---|
| 662 | if not os.path.isdir(dir_path): |
|---|
| 663 | raise validate.VdtValueError( |
|---|
| 664 | "%s (plugin directory not found)" % one_dir) |
|---|
| 665 | if not os.access(dir_path, os.X_OK): |
|---|
| 666 | raise validate.VdtValueError( |
|---|
| 667 | "%s (access denied for plugin directory)" % one_dir) |
|---|
| 668 | result.append(dir_path) |
|---|
| 669 | return result |
|---|
| 670 | |
|---|
| 671 | |
|---|
| 672 | class MiscConfigFile: |
|---|
| 673 | """all other config files (e.g. a ssl certificate) to be stored""" |
|---|
| 674 | |
|---|
| 675 | maxSize = 20480 |
|---|
| 676 | |
|---|
| 677 | def __init__(self, filename, logger): |
|---|
| 678 | self.filename = filename |
|---|
| 679 | self.log = logger |
|---|
| 680 | self.content = None |
|---|
| 681 | self.load() |
|---|
| 682 | |
|---|
| 683 | |
|---|
| 684 | def load(self): |
|---|
| 685 | """Load a configuration file into memory. |
|---|
| 686 | """ |
|---|
| 687 | fdesc = open(self.filename, "rb") |
|---|
| 688 | |
|---|
| 689 | self.content = fdesc.read(self.maxSize) |
|---|
| 690 | if fdesc.tell() == self.maxSize: |
|---|
| 691 | self.log.warn("file in misc settings directory (" + str(self.filename) \ |
|---|
| 692 | + ") is bigger than allowed (" + str(self.maxSize) + ")") |
|---|
| 693 | fdesc.close() |
|---|
| 694 | |
|---|
| 695 | |
|---|
| 696 | def save(self): |
|---|
| 697 | """Save a configuration file to disk. |
|---|
| 698 | """ |
|---|
| 699 | |
|---|
| 700 | if os.path.exists(self.filename) and not os.access(self.filename, os.W_OK): |
|---|
| 701 | return True |
|---|
| 702 | save_dir = os.path.dirname(self.filename) |
|---|
| 703 | |
|---|
| 704 | if not os.path.isdir(save_dir): |
|---|
| 705 | try: |
|---|
| 706 | os.mkdir(save_dir) |
|---|
| 707 | except IOError: |
|---|
| 708 | return False |
|---|
| 709 | |
|---|
| 710 | try: |
|---|
| 711 | fdesc = open(self.filename, "wb") |
|---|
| 712 | except IOError: |
|---|
| 713 | return False |
|---|
| 714 | try: |
|---|
| 715 | fdesc.write(self.content) |
|---|
| 716 | fdesc.close() |
|---|
| 717 | return True |
|---|
| 718 | except IOError: |
|---|
| 719 | fdesc.close() |
|---|
| 720 | return False |
|---|
| 721 | |
|---|
| 722 | |
|---|
| 723 | |
|---|
| 724 | class LocalSysLogHandler(logging.Handler): |
|---|
| 725 | """Pass logging messages to a local syslog server without unix sockets. |
|---|
| 726 | |
|---|
| 727 | derived from: logging.SysLogHandler |
|---|
| 728 | """ |
|---|
| 729 | |
|---|
| 730 | def __init__(self, prepend='CryptoBox', facility=syslog.LOG_USER): |
|---|
| 731 | logging.Handler.__init__(self) |
|---|
| 732 | self.formatter = None |
|---|
| 733 | self.facility = facility |
|---|
| 734 | syslog.openlog(prepend, 0, facility) |
|---|
| 735 | |
|---|
| 736 | |
|---|
| 737 | def close(self): |
|---|
| 738 | """close the syslog connection |
|---|
| 739 | """ |
|---|
| 740 | syslog.closelog() |
|---|
| 741 | logging.Handler.close(self) |
|---|
| 742 | |
|---|
| 743 | |
|---|
| 744 | def emit(self, record): |
|---|
| 745 | """format and send the log message |
|---|
| 746 | """ |
|---|
| 747 | msg = "%s: %s" % (record.levelname, record.getMessage()) |
|---|
| 748 | try: |
|---|
| 749 | syslog.syslog(record.levelno, msg) |
|---|
| 750 | except Exception: |
|---|
| 751 | self.handleError(record) |
|---|