def wrapper(*args, **kwargs): if not state.initialized: if config.get('debug', False) and config.get('debug.secret_key'): secret_key_file = config.get('debug.secret_key') crypto_util.load_secret_key_file(secret_key_file) else: raise exc.CryptoNotInitialized("Crypto engine has not been initialized.") return f(*args, **kwargs)
def init_crypto(options): """ Interactive target to initialize the database with a new crypto passphrase. """ print "Initializing crypto for an empty database." if crypto_util.has_encrypted_data(): raise BuildFailure("Database has existing encrypted contents; use the 'rekey' target instead.") passphrase = raw_input("Passphrase: ") print "The database will be initialized with the passphrase between the arrows: --->%s<---" % passphrase print "The MD5 of the passphrase you entered is: %s" % hashlib.md5(passphrase).hexdigest() confirm = raw_input("Type 'YES' to confirm passphrase and MD5 are correct: ") if confirm != 'YES': raise ValueError("You must enter 'YES' to proceed.") salt = get_random_bytes(16) key = crypto_util.derive_key(passphrase=passphrase, salt=salt) crypto_util.initialize_key_metadata(key=key, salt=salt, force_overwrite=False) print "Database key metadata has been initialized. Your application is ready for use." if config.get('debug'): print "The new key is: %s%s" % (binascii.hexlify(key.encryption_key), binascii.hexlify(key.signing_key)) print "*************************************************************" print "IMPORTANT" print "Make sure your database master passphrase is stored somewhere" print "outside of Ensconce." print "" print "There is no recovery mechanism for this passphrase (or for " print "your database, should you lose it.)" print "*************************************************************"
def rekey(options): """ Interactive target to change the passphrase for the database. """ info("This is an EXTREMELY DANGEROUS activity.") info("Backup your database first.") curr_passphrase = raw_input("Current passphrase: ") crypto_util.configure_crypto_state(curr_passphrase) new_passphrase = raw_input("New passphrase: ") confirm = raw_input("MD5 of passphrase is %s (type \"YES\" to confirm): " % hashlib.md5(new_passphrase).hexdigest()) if confirm != 'YES': raise ValueError("You must enter 'YES' to proceed.") if crypto_util.has_encrypted_data(): confirm = raw_input("There is existing encrypted data in the database. Type 'REKEY' to proceed with re-encryption: ") if confirm != 'REKEY': raise ValueError("You must enter 'REKEY' to proceed.") # Use the same salt as previous key new_key = crypto_util.derive_configured_key(new_passphrase) crypto_util.replace_key(new_key=new_key, force=True) info("Re-encryption completed successfully.") if config.get('debug'): print "The new key is: %s%s" % (binascii.hexlify(new_key.encryption_key), binascii.hexlify(new_key.signing_key))
def backup_database(): """ Backups entire database contents to a YAML file which is encrypted using the password from a specified mapped password in the database. """ try: dir_mode = int(config.get("backups.dir_mode", "0700"), 8) file_mode = int(config.get("backups.file_mode", "0600"), 8) # Before we do anything, validate the configuration. if not os.path.exists(config["backups.path"]): # Attempt to make the directories. os.makedirs(config["backups.path"], mode=dir_mode) if not config.get("backups.encryption.password_id"): raise exc.ConfigurationError( "Cannot backup without configured password_id (backups.encryption.password_id)" ) try: pw = passwords.get(config["backups.encryption.password_id"]) except exc.NoSuchEntity as x: raise exc.ConfigurationError( "Configured backups.encryption.password_id does not exist in database: {0}".format(x) ) backup_fname = datetime.now().strftime("backup-%Y-%m-%d-%H-%M.gpg") msg = "Backing up database to {fname}, secured by password id={pw.id}, resource={resource.name}[{resource.id}]" log.info(msg.format(fname=backup_fname, pw=pw, resource=pw.resource)) exporter = GpgYamlExporter(passphrase=pw.password_decrypted, use_tags=True, include_key_metadata=True) encrypted_stream = BytesIO() exporter.export(stream=encrypted_stream) encrypted_stream.seek(0) # Just to ensure it's rewound backup_file = os.path.join(config["backups.path"], backup_fname) with open(backup_file, "w") as fp: fp.write(encrypted_stream.read()) os.chmod(backup_file, file_mode) except: log.critical("Error backing up database.", exc_info=True) raise
def setUpClass(cls): """ Initialize the database. We also read and store the encryption key here (but actual state setup happens in in setUp method). """ super(BaseModelTest, cls).setUpClass() init_model(config) if not config.get('debug.secret_key'): raise Exception("Tests only work with debug.secret_key set to a valid key.") cls.SECRET_KEY = CombinedMasterKey(binascii.unhexlify(open(config['debug.secret_key']).read().strip()))
def test_data(options): """ Erases database and replaces contents with test data. (This will only work if debug is on and debug secret key is set.) """ if not config['debug'] or not config['debug.secret_key']: raise BuildFailure("This target only works with debug=True and debug.secret_key set") else: secret_key_file = config.get('debug.secret_key') crypto_util.load_secret_key_file(secret_key_file) #init_model(config) data = populate.TestDataPopulator() data.populate()
def run_server(argv=None): if argv is None: argv = sys.argv parser = optparse.OptionParser(description='Run the ensconce cherrypy server.') init_app() parser.add_option('-d', '--debug', default=config.get('debug', False), action="store_true", help='Run in debug mode?') (options, args) = parser.parse_args() config['debug'] = options.debug server.configure() server.serve_forever()
def render(filename, data=None): """ Convenience method to render a template. """ from ensconce import acl if data is None: data = {} data['title_prefix'] = config.get('ui.title_prefix') data['operator_info'] = operator_info() env = Environment(loader=PackageLoader('ensconce', 'templates'), autoescape=True, finalize=lambda x: '' if x is None else x) env.globals['pop_notifications'] = pop_notifications # Expose the ACL module so that there can be some checking in the templates # (to avoid showing buttons that won't be clickable) env.globals['acl'] = acl try: env.globals['app_version'] = pkg_resources.get_distribution("ensconce").version except: log.exception("Error determining software version.") env.globals['app_version'] = '?.?' # Add an escape filter for when we need to embed values in JS code. env.filters['escapejs'] = escapejs if operator_info().user_id: # They are logged in, so add the quick-group-nav form. form = QuickGroupForm() # Do not initialize we/ request params, since that could be confusing. form.group_id.choices = [(0, '[Jump to Group]')] + [(g.id, g.name) for g in groups.list()] env.globals['quickgroupform'] = form return env.get_template(filename).render(data)
def export(self, group_id=None, **kwargs): form = ExportForm(request_params(), group_id=group_id) form.group_id.choices = [(g.id, g.name) for g in groups.list()] exporter_choices = [('yaml', 'YAML (GPG/PGP-encrypted)')] if config['export.keepass.enabled']: if not os.path.exists(config['export.keepass.exe_path']): log.error("KeePass export enabled, but specified converter script does not exist: {0}".format(config.get('export.keepass.exe_path'))) else: exporter_choices.append(('kdb', 'KeePass 1.x')) form.format.choices = exporter_choices if cherrypy.request.method == 'POST': if form.validate(): group = groups.get(form.group_id.data) if form.format.data == 'yaml': exporter = GpgYamlExporter(use_tags=False, passphrase=form.passphrase.data, resource_filters=[model.GroupResource.group_id==group.id]) # @UndefinedVariable encrypted_stream = BytesIO() exporter.export(stream=encrypted_stream) encrypted_stream.seek(0) # Just to ensure it's rewound return serve_fileobj(encrypted_stream, content_type='application/pgp-encrypted', disposition='attachment', name='group-{0}-export.pgp'.format(re.sub('[^\w\-\.]', '_', group.name))) elif form.format.data == 'kdb': exporter = KeepassExporter(passphrase=form.passphrase.data, resource_filters=[model.GroupResource.group_id==group.id]) # @UndefinedVariable encrypted_stream = BytesIO() exporter.export(stream=encrypted_stream) encrypted_stream.seek(0) # Just to ensure it's rewound return serve_fileobj(encrypted_stream, content_type='application/x-keepass-database', disposition='attachment', name='group-{0}-export.kdb'.format(re.sub('[^\w\-\.]', '_', group.name))) else: # I don't think we can get here in normal business. raise RuntimeError("Unhandled format specified: {0}".format(form.format.data)) else: # does not validate return render("group/export.html", {'form': form}) else: # request method is GET return render("group/export.html", {'form': form})
def configure(): """ Configures the cherrypy server (sets up the tree, cherrypy config, etc.). """ global configured # Setup the session storage directory if it does not exist if config.get('sessions.on') and config.get('sessions.storage_type') == 'file': path = config['sessions.storage_path'] if not os.path.exists(path): try: os.makedirs(path) # By default these will be 0777 except: warnings.warn("Unable to create the session directory: {0}".format(path)) cherrypy.config.update({ "server.socket_host": config['server.socket_host'], "server.socket_port": config['server.socket_port'], "checker.on": False, "log.screen": False, #"engine.autoreload_on": False, "engine.autoreload_on": config.as_bool("debug"), "tools.sessions.on": config.as_bool('sessions.on'), "tools.sessions.persistent": config.as_bool('sessions.persistent'), "tools.sessions.path": config['sessions.path'], "tools.sessions.timeout": config['sessions.timeout'], "tools.sessions.storage_type": config['sessions.storage_type'], "tools.sessions.storage_path": config['sessions.storage_path'], "tools.sessions.secure": config['sessions.secure'], "request.show_tracebacks": config.as_bool("debug"), "checker.on": False, "tools.caching.on": False, "tools.expires.on": True, "tools.expires.secs": 0, "tools.expires.force": True, "tools.log_headers.on": False, "engine.autoreload_on": config.as_bool("debug"), "tools.encode.on": True, "tools.encode.encoding": "utf8", "error_page.default": error_handler }) if config['server.behind_proxy']: cherrypy.config.update({"tools.proxy.on": True}) if config['server.ssl_certificate']: # Make this conditional so we can host behind apache? cherrypy.config.update({ "server.ssl_certificate": config['server.ssl_certificate'], "server.ssl_private_key": config['server.ssl_private_key'], "server.ssl_certificate_chain": config['server.ssl_certificate_chain'], }) def rollback_dbsession(): log.info("Rolling back SA transaction.") session = meta.Session() session.rollback() def commit_dbsession(): log.info("Committing SA transaction.") session = meta.Session() session.commit() cherrypy.tools.dbsession_rollback = cherrypy.Tool('before_error_response', rollback_dbsession) cherrypy.tools.dbsession_commit = cherrypy.Tool('on_end_resource', commit_dbsession) # This is a "flow-control" exception. class _LoginFailed(Exception): pass # TODO: Refactor to combine with the ensconce.webapp.tree methods def checkpassword(realm, username, password): auth_providers = get_configured_providers() try: for auth_provider in auth_providers: try: auth_provider.authenticate(username, password) except exc.InsufficientPrivileges: # Fail fast in this case; we don't want to continue on to try other authenticators. raise _LoginFailed() except exc.AuthError: # Swallow other auth errors so it goes onto next authenticator in the list. pass except: # Other exceptions needs to get logged at least. log.exception("Unexpected error authenticating user using {0!r}".format(auth_provider)) else: log.info("Authentication succeeded for username {0} using provider {1}".format(username, auth_provider)) break else: log.debug("Authenticators exhausted; login failed.") raise _LoginFailed() except _LoginFailed: auditlog.log(auditlog.CODE_AUTH_FAILED, comment=username) return False else: # Resolve the user using the *current value* for auth_provider (as that is the one that passed the auth. user = auth_provider.resolve_user(username) log.debug("Setting up cherrypy session with username={0}, user_id={1}".format(username, user.id)) cherrypy.session['username'] = username # @UndefinedVariable cherrypy.session['user_id'] = user.id # @UndefinedVariable auditlog.log(auditlog.CODE_AUTH_LOGIN) return True app_conf = { "/static": { "tools.staticdir.on": True, "tools.staticdir.dir": config['static_dir'], "tools.staticdir.index": "index.html" }, "/jsonrpc": { 'tools.auth_basic.on': True, 'tools.auth_basic.realm': 'api', 'tools.auth_basic.checkpassword': checkpassword, } } # Add a plugin that will run the Crypto.Random.atfork() method, since this must # be called after forking (and we run this as a daemon in production) util.RNGInitializer(cherrypy.engine).subscribe() # Wire up our daemon tasks background_tasks = [] if config.get('sessions.on'): background_tasks.append(tasks.DaemonTask(tasks.remove_old_session_files, interval=60)) if config.get('backups.on'): backup_interval = config['backups.interval_minutes'] * 60 background_tasks.append(tasks.DaemonTask(tasks.backup_database, interval=backup_interval, wait_first=True)) background_tasks.append(tasks.DaemonTask(tasks.remove_old_backups, interval=3600, wait_first=True)) # This checks a day-granularity interval internally. # Unsubscribe anything that is already there, so that this method is idempotent # (This surfaces as nasty bugs in testing otherwise.) for channel in cherrypy.engine.listeners: for callback in cherrypy.engine.listeners[channel]: log.debug("Unsubscribing {0}:{1!r}".format(channel, callback)) # log.debug("Unsubscribing {0}:{1!r}".format(channel, callback)) # cherrypy.engine.unsubscribe(channel, callback) for task in background_tasks: cherrypy.engine.subscribe("start", task.start, priority=99) cherrypy.engine.subscribe("stop", task.stop) # Setup the basic/top-level webapp API root = tree.Root() # Iterate over all the modules in the ensconce.webapp.tree package and add # their 'Root' classes to the tree pkgpath = os.path.dirname(tree.__file__) for modname in [name for (_, name, _) in pkgutil.iter_modules([pkgpath])]: module = __import__("ensconce.webapp.tree." + modname, fromlist=["Root"]) module_root = module.Root() setattr(root, modname, module_root) # I think this is here because we want to explicitly specify the ServerAdapter below # rather than use a default one. cherrypy.server.unsubscribe() app = cherrypy.tree.mount(root, "/", app_conf) app.log.error_log.level = cherrypy.log.error_log.level # @UndefinedVariable app.log.access_log.level = cherrypy.log.access_log.level # @UndefinedVariable addr = (config["server.socket_host"], config["server.socket_port"]) server = CherryPyWSGIServer(addr, app, numthreads=50, timeout=2) # TODO: make numthreads and keepalive timeout configurable # TODO: This is also mentioned in the cherrypy config above .... ? One of these is probably redundant. server.ssl_certificate = config["server.ssl_certificate"] server.ssl_private_key = config["server.ssl_private_key"] if config["server.ssl_certificate_chain"]: server.ssl_certificate_chain = config["server.ssl_certificate_chain"] adapter = ServerAdapter(cherrypy.engine, server, server.bind_addr) adapter.subscribe() configured = True