예제 #1
0
파일: util.py 프로젝트: EliAndrewC/ensconce
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.")
예제 #2
0
 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
예제 #3
0
 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))
예제 #4
0
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")
예제 #5
0
    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("/")
예제 #6
0
 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})
예제 #7
0
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
예제 #8
0
 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)
예제 #9
0
 def rollback_dbsession():
     log.info("Rolling back SA transaction.")
     session = meta.Session()
     session.rollback()
예제 #10
0
 def commit_dbsession():
     log.info("Committing SA transaction.")
     session = meta.Session()
     session.commit()
예제 #11
0
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.
예제 #12
0
 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