def load_secret_key_file(secret_key_file): """ Loads a secret key from a file and initializes the engine with this key. This is designed for use in development/debugging and should NOT be used in production, if you value the encrypted database data. :param secret_key_file: The path to a file containing the 32-byte secret key. :type secret_key_file: str :raise ensconce.exc.CryptoNotInitialized: If the engine cannot be initialized. """ try: with open(secret_key_file) as fp: key_bytes = binascii.unhexlify(fp.read().strip()) log.info("Using DEBUG secret.key from file: {0}".format(secret_key_file)) try: secret_key = CombinedMasterKey(key_bytes) validate_key(key=secret_key) except exc.MissingKeyMetadata: log.info("Writng out DEBUG secret.key to key metadata row.") initialize_key_metadata(key=secret_key, salt=get_random_bytes(16)) state.secret_key = secret_key except: log.exception("Unable to initialize secret key from file.") raise exc.CryptoNotInitialized("Crypto engine has not been initialized.")
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
def getPasswordForResource(self, username, resource_id, include_history=False): """ Looks up a password matching specified username @ specified resource name (e.g. hostname). :param username: The username associated with the password. :type username: str :param resource_id: The resource ID or name that we are looking up. :type resource_id: int or str :param include_history: Whether to include history (previous passwords) for this password. :type include_history: bool :return: The matching password, or None if none found. """ try: try: resource_id = int(resource_id) except ValueError: resource = resources.get_by_name(resource_id, assert_single=True) else: resource = resources.get(resource_id) pw = passwords.get_for_resource(username=username, resource_id=resource.id, assert_exists=True) auditlog.log(auditlog.CODE_CONTENT_VIEW, target=pw) return pw.to_dict(decrypt=True, include_history=include_history) except exc.NoSuchEntity: log.info("Unable to find password matching user@resource: {0}@{1}".format(username, resource_id)) raise except: log.exception("Unable to find password for resource.") raise RuntimeError("Unhandled error trying to lookup password for user@resource: {0}@{1}".format(username, resource_id))
def serve_forever(): """ Run the [already-configured] cherrypy server forever. """ cherrypy.engine.start() try: cherrypy.engine.block() except KeyboardInterrupt: log.info("shutting down due to KeyboardInterrupt")
def process_login(self, **kwargs): form = LoginForm(request_params()) # TODO: Refactor to combine with the ensconce.server:checkpassword method. Lots of duplicate # logic here. AT MINIMUM MAKE SURE THAT ANY CHANGES HERE ARE REFLECTED THERE # This is a "flow-control" exception. ... You'll see. :) class _LoginFailed(Exception): pass try: if not form.validate(): raise _LoginFailed() username = form.username.data password = form.password.data for auth_provider in get_configured_providers(): try: auth_provider.authenticate(username, password) except exc.InsufficientPrivileges: form.username.errors.append(ValidationError("Insufficient privileges to log in.")) # 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.") form.password.errors.append(ValidationError("Invalid username/password.")) raise _LoginFailed() except _LoginFailed: auditlog.log(auditlog.CODE_AUTH_FAILED, comment=username) return render("login.html", {'auth_provider': config['auth.provider'], 'form': form}) 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) if form.redirect.data: raise cherrypy.HTTPRedirect(form.redirect.data) else: raise cherrypy.HTTPRedirect("/")
def process_merge(self, **kwargs): form = MergeForm(request_params()) group_tuples = [(g.id, g.name) for g in groups.list()] form.from_group_id.choices = [(0, '[From Group]')] + group_tuples form.to_group_id.choices = [(0, '[To Group]')] + group_tuples if form.validate(): log.info("Passed validation, somehow.") (moved_resources, from_group, to_group) = groups.merge(form.from_group_id.data, form.to_group_id.data) for r in moved_resources: auditlog.log(auditlog.CODE_CONTENT_MOD, target=r, attributes_modified=['group_id']) auditlog.log(auditlog.CODE_CONTENT_DEL, target=from_group) raise cherrypy.HTTPRedirect('/group/view/{0}'.format(to_group.id)) else: return render("group/merge.html", {'form': form})
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 export(self, stream): """ """ gpg_filename = None kdb_filename = None try: with tempfile.NamedTemporaryFile(suffix='.yaml.gpg', prefix='export', delete=False) as gpg_fp: super(KeepassExporter, self).export(gpg_fp) gpg_filename = gpg_fp.name kdb_fp = tempfile.NamedTemporaryFile(suffix='.kdb', prefix='export', delete=False) # We have to manually delete this one kdb_filename = kdb_fp.name kdb_fp.close() cmd_exe = config['export.keepass.exe_path'] args = ['-i', gpg_filename, '-o', kdb_filename] log.info("Executing command: {0} {1}".format(cmd_exe, ' '.join(args))) child = pexpect.spawn(cmd_exe, args) child.expect('ssphrase') child.sendline(self.passphrase) child.expect(pexpect.EOF) log.debug(child.before) with open(kdb_filename) as read_fp: # Read contents of file into our own stream kdb_bytes = read_fp.read() stream.write(kdb_bytes) log.debug("Read {0} bytes from kdb file stream".format(len(kdb_bytes))) stream.seek(0) finally: if gpg_filename: os.remove(gpg_filename) if kdb_filename: os.remove(kdb_filename)
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()
def init_model(config, drop=False, check_version=True): """ Initializes the tables and classes of the model using specified engine. You must call this method before using any of the tables or classes in the model! :param config: The application configurat ion object, where SA config is prefixed with "sqlalchemy." :type config: dict :param drop: Whether to drop the tables first. :type drop: bool :param check_version: Whether to ensure that the database version is up-to-date. :type check_version: bool """ engine = engine_from_config(config) sm = orm.sessionmaker(autoflush=True, autocommit=False, bind=engine) meta.engine = engine meta.Session = orm.scoped_session(sm) alembic_cfg = migrationsutil.create_config() # Check to see whether the database has already been created or not. # Based on this, we know whether we need to upgrade the database or mark the database # as the latest version. inspector = Inspector.from_engine(engine) # We choose an arbitrary table name here to see if database objects have been created db_objects_created = (groups_table.name in inspector.get_table_names()) fresh_db = False if not db_objects_created: log.info("Database apears uninitialized, creating database tables") meta.metadata.create_all(engine, checkfirst=True) fresh_db = True elif drop: log.info("Dropping database tables and re-creating.") meta.metadata.drop_all(engine, checkfirst=True) meta.metadata.create_all(engine) fresh_db = True if fresh_db: command.stamp(alembic_cfg, "head") else: # Existing model may need upgrade. if check_version: latest = migrationsutil.get_head_version() installed = migrationsutil.get_database_version() if latest != installed: raise DatabaseVersionError("Installed database ({0}) does not match latest available ({1}). (Use the `paver upgrade_db` command.)".format(installed, latest)) else: log.info("Skipping database upgrade.") if check_version: # We only want to run this code if this is a normal version-checking model initialization. # Basically we do *not* want to run this when we are upgrading the database. # (This may need to get refactored to be clearer.) # # Special check for acl data (this is kinda kludgy, but app won't work if this row isn't here.) s = meta.Session() a = s.query(Access).get(1) if not a: # Can't import this at top-level due to circular from ensconce import acl a = Access() a.description = 'Administrator' a.level = acl.ALL_ACCESS s.add(a) s.commit() # We really do want to commit immediately in this particular case.
def from_structure(self, structure): """ Populates the SQLAlchemy model from a python dictionary of the database structure. """ session = meta.Session() try: for resource_s in structure['resources']: log.debug("Importing: {0!r}".format(resource_s)) # First build up a list of group_ids for this resource that will correspond to groups # in *this* database. group_ids = [] for gname in resource_s['groups']: group = groups.get_by_name(gname, assert_exists=False) if not group: group = groups.create(gname) log.info("Created group: {0!r}".format(group)) else: log.info("Found existing group: {0!r}".format(group)) group_ids.append(group.id) # First we should see if there is a match for the id and name; we can't rely on name alone since # there is no guarantee of name uniqueness (even with a group) resource = None resource_candidate = resources.get(resource_s['id'], assert_exists=False) if resource_candidate and resource_candidate.name == resource_s['name']: resource = resource_candidate else: # If we find a matching resource (by name) and there is only one then we'll use that. try: resource = resources.get_by_name(resource_s['name'], assert_single=True, assert_exists=True) except MultipleResultsFound: log.info("Multiple resource matched name {0!r}, will create a new one.".format(resource_s['name'])) except exc.NoSuchEntity: log.debug("No resource found matching name: {0!r}".format(resource_s['name'])) pass resource_attribs = ('name', 'addr', 'description', 'notes', 'tags') resource_attribs_update = dict([(k,v) for (k,v) in resource_s.items() if k in resource_attribs]) if resource: (resource, modified) = resources.modify(resource.id, group_ids=group_ids, **resource_attribs_update) # (yes, we are overwriting 'resource' var with new copy returned from this method) log.info("Updating existing resource: {0!r} (modified: {1!r})".format(resource, modified)) if modified and modified != ['group_ids']: if not self.force: raise RuntimeError("Refusing to modify existing resource attributes {0!r} on {1!r} (use 'force' to override this).".format(modified, resource)) else: log.warning("Overwriting resource attributes {0!r} on {1!r}".format(modified, resource)) else: # We will just assume that we need to create the resource. Yes, it's possible it'll match an existing # one, but better to build a merge tool than end up silently merging things that are not the same. resource = resources.create(group_ids=group_ids, **resource_attribs_update) log.info("Created new resource: {0!r}".format(resource)) # Add the passwords for password_s in resource_s['passwords']: password_attribs = ('username', 'description', 'password', 'tags') password_attribs_update = dict([(k,v) for (k,v) in password_s.items() if k in password_attribs]) # Look for a matching password. We do know that this is unique. password = passwords.get_for_resource(password_s['username'], password_s['resource_id'], assert_exists=False) if password: (password, modified) = passwords.modify(password_id=password.id, **password_attribs_update) # (Yeah, we overwrite password object.) log.info("Updating existing password: {0!r} (modified: {1!r})".format(password, modified)) non_pw_modified = set(modified) - set(['password']) if not modified: log.debug("Password row not modified.") else: log.debug("Password modified: {0!r}".format(modified)) # If anything changed other than password, we need to ensure that force=true if non_pw_modified: if not self.force: raise RuntimeError("Refusing to modify existing password attributes {0!r} on {1!r} (use 'force' to override this).".format(non_pw_modified, password)) else: log.warning("Overwriting password attributes {0!r} on {1!r}".format(non_pw_modified, password)) else: password = passwords.create(resource_id=resource.id, **password_attribs_update) log.info("Creating new password: {0!r}".format(password)) # This probably isn't necessary as all the DAO methods should also flush session, but might as well. session.flush() except: session.rollback() raise