Example #1
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))
Example #2
0
def list(): # @ReservedAssignment
    """
    This function will return a list of all resources.
    
    :param offset: Offset in list for rows to return (supporting pagination).
    :type offset: int
    
    :param limit: Max rows to return (supporting pagination).
    :type limit: int
    
    :returns: A list of :class:`ensconce.model.Resource` results.
    :rtype: list
    """
    session = meta.Session()
    try:
        r_t = model.resources_table
        q = session.query(model.Resource)
        q = q.order_by(r_t.c.name)
        
        resources = q.all()
    except:
        log.exception("Error listing resources")
        raise
    else:
        return resources
Example #3
0
def validate_key(key):
    """
    Checks the key against an encrypted blob in the database.
    
    :param key: The master key to check.
    :type key: ensconce.crypto.MasterKey
    
    :raise ensconce.exc.MissingKeyMetadata: If the metadata row does not exist yet.
    :raise ensconce.exc.MultipleKeyMetadata: If there are multiple metadata rows.
    :raise ensconce.exc.UnconfiguredModel: If we can't create an SA session.
    """
    if meta.Session is None:
        raise exc.UnconfiguredModel()

    session = meta.Session()
    try:
        key_info = session.query(model.KeyMetadata).one()
        # log.debug("Got bytes for validation: {0!r}".format(key_info.validation))
        try:
            decrypted = engine.decrypt(key_info.validation, key=key)
            # log.debug("Decrypts to: {0!r}".format(decrypted))
        except exc.CryptoAuthenticationFailed:
            log.exception("Validation fails due to error decrypting block.")
            return False
        else:
            return True
    except NoResultFound:
        raise exc.MissingKeyMetadata()
    except MultipleResultsFound:
        raise exc.CryptoError("Multiple key metadata rows are not supported.")
    except:
        log.exception("Error validating encryption key.")
        raise
Example #4
0
def initialize_key_metadata(key, salt, force_overwrite=False, nested_transaction=False):
    """
    Called when key is first specified to set some database encrypted contents.
    
    This must be run before the crypto engine has been initialized with the secret
    key.
    
    :param key: The new encryption and signing key set.
    :type key: ensconce.crypto.MasterKey
    
    :param salt: The salt to use for the KDF function.  IMPORTANT: This cannot change w/o re-encrypting database.
    :type salt: str
    
    :param force_overwrite: Whether to delete any existing metadata first (dangerous!)
    :type force_overwrite: bool
    
    :param nested_transaction: Whether this is being run within an existing transaction (i.e. do not commit).
    :type nested_transaction: bool
    
    :raise ensconce.exc.CryptoAlreadyInitialized: If the engine has already been initialized we bail out.
    :raise ensconce.exc.UnconfiguredModel: If we can't create an SA session.
    :raise ensconce.exc.ExistingKeyMetadata: If there is already key metadata (and `force_overwrite` param is not `True`).
    """
    assert isinstance(key, MasterKey)
    assert isinstance(salt, str)

    if state.initialized:
        raise exc.CryptoAlreadyInitialized()

    if meta.Session is None:
        raise exc.UnconfiguredModel()

    session = meta.Session()
    try:
        existing_keys = session.query(model.KeyMetadata).all()
        if len(existing_keys) > 0:
            if force_overwrite:
                for ek in existing_keys:
                    session.delete(ek)
                    log.warning("Forcibly removing existing metadata: {0}".format(ek))
                session.flush()
            else:
                raise exc.ExistingKeyMetadata()

        km = model.KeyMetadata()
        km.id = 0  # Chosen to be obviously out of auto-increment "range"
        km.validation = create_key_validation_payload(key=key)
        km.kdf_salt = salt
        session.add(km)
        if not nested_transaction:
            session.commit()  # We are deliberately committing early here
        else:
            session.flush()
    except:
        if not nested_transaction:
            # This conditional probably has little effect, since the connection will be in err state anyway
            # until a rollback is issued.
            session.rollback()
        log.exception("Error initializing key metadata")
        raise
Example #5
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
Example #6
0
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.")
Example #7
0
def create(username, password=None, access_id=None, externally_managed=False):
    """
    This function will create an operator record in the database.
    
    :rtype: :class:`ensconce.model.Operator`
    """
    check = get_by_username(username, assert_exists=False)
    # First, check to see if the given username exists.
    if check:
        raise ValueError("User already exists: {0}".format(username))

    # Force the password to be null if it is empty (prevent logins w/ empty password)
    if password == "":
        password = None

    session = meta.Session()
    try:
        operator = model.Operator()
        operator.username = username
        if password is not None:
            operator.password = pwhash.obscure(password)
        operator.access_id = access_id
        operator.externally_managed = externally_managed
        session.add(operator)
        session.flush()
    except:
        log.exception("Error saving new operator_id.")
        raise

    return operator
Example #8
0
def modify(operator_id, **kwargs):
    """
    This function will attempt to modify the operator with the passed
    in values.
    
    :keyword username: The username for this operator.
    :keyword password: The password for this operator.
    :keyword access_id: The associated access level id for this operator. 
    """
    session = meta.Session()
    update_attributes = kwargs  # Just to make it clearer

    log.debug("Update attribs = %r" % update_attributes)

    # Force the password to be null if it is empty (prevent logins w/ empty password)
    if update_attributes.get("password") == "":
        update_attributes["password"] = None

    try:
        operator = get(operator_id)
        modified = model.set_entity_attributes(operator, update_attributes, hashed_attributes=["password"])
        session.flush()
    except:
        log.exception("Error modifying operator: {0}".format(operator_id))
        raise
    return (operator, modified)
Example #9
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("/")
Example #10
0
def modify(resource_id, group_ids=None, **kwargs):
    """
    This function will modify a resource entry in the database, only updating
    specified attributes.
    
    :param resource_id: The ID of resource to modify.
    :keyword group_ids: The group IDs that this resource should belong to.
    :keyword name: The resource name.
    :keyword addr: The resource address.
    :keyword notes: An (encrypted) notes field.
    :keyword tags: The tags field.
    :keyword description: A description fields (not encrypted).
    """
    if isinstance(group_ids, (basestring,int)):
        group_ids = [int(group_ids)]
    
    if group_ids is not None and len(group_ids) == 0:
        raise ValueError("Cannot remove all groups from a resource.")
    
    session = meta.Session()
    
    resource = get(resource_id)
    
    update_attributes = kwargs
    
    try:
        modified = model.set_entity_attributes(resource, update_attributes, encrypted_attributes=['notes'])
        session.flush()
    except:
        log.exception("Error updating resource.")
        raise
    
    gr_t = model.group_resources_table
    
    if group_ids is not None:
        # Is it different from what's there now?
        if set(group_ids) != set([cg.id for cg in resource.groups]):
            try:
                session.execute(gr_t.delete(gr_t.c.resource_id==resource_id))
                for group_id in group_ids:
                    group_id = int(group_id)
                    gr = model.GroupResource()
                    gr.resource_id = resource.id
                    gr.group_id = group_id
                    session.add(gr)
                session.flush()
            except:
                log.exception("Error adding group memberships")
                raise
            else:
                modified += ['group_ids']
    
    session.flush()
    
    return (resource, modified)
Example #11
0
def list():  # @ReservedAssignment
    """
    This function will return all of the operators in the system.
    """
    session = meta.Session()
    try:
        operators = session.query(model.Operator).order_by(model.Operator.username).all()  # @UndefinedVariable
    except:
        log.exception("Error loading operator list.")
        raise
    else:
        return operators
Example #12
0
    def run(self):

        if self.wait_first and not self.stopped.is_set():
            self.stopped.wait(self.interval)

        while not self.stopped.is_set():
            try:
                self.func()
            except:
                log.exception("Error executing daemon function.")

            if self.interval:
                self.stopped.wait(self.interval)
Example #13
0
def delete(password_id):
    """
    This function will attempt to delete a operatorid from the database.
    """
    session = meta.Session()
    try:
        operator = get(password_id)
        session.delete(operator)
    except:
        log.exception("Unable to delete operator: {0}".format(password_id))
        raise

    return operator
Example #14
0
def delete(resource_id):
    """
    This function will delete a host record from the database.
    """
    session = meta.Session()
    try:
        resource = get(resource_id)
        session.delete(resource)
        session.flush()
    except:
        log.exception("Error deleting resource: {0}".format(resource_id))
        raise
    return resource
Example #15
0
def recent_content_views(operator_id, object_type, code=None, object_id=None, limit=10, limit_days=7, skip_count=False):
    """
    
    """
    # This is not very efficient yet.  The right way to do this is probably to 
    # map a subclass of AuditLogEntry to a query that groups by object_id, object_type, and code 
    # showing the max timestamp.
    
    session = meta.Session()
    
    try:
        a_t = model.auditlog_table
        subq = session.query(func.max(a_t.c.id)).group_by(a_t.c.object_type,
                                                          a_t.c.object_id,
                                                          a_t.c.code,
                                                          a_t.c.operator_id)
        
        clauses = []
        clauses.append(a_t.c.operator_id==operator_id)
        clauses.append(a_t.c.object_type==object_type)
        # In an attempt to improve efficiency:
        clauses.append(a_t.c.datetime>=datetime.now()-timedelta(days=limit_days))
        
        
        
        if object_id:
            clauses.append(a_t.c.object_id==object_id)
        
        if code:
            clauses.append(a_t.c.code==code)
        
        subq = subq.filter(and_(*clauses))
        
        # Now get all the actual audit log rows that match that.  Order by the datetime descending.
        q = session.query(model.AuditlogEntry).filter(a_t.c.id.in_(subq))
        q = q.order_by(a_t.c.datetime.desc())
        
        if not skip_count:
            count = q.count()
        else:
            count = 0 
        
        q = q.limit(limit)
        
        results = q.all() 
        
    except:
        applog.exception("Error searching audit log.")
        raise
    
    return SearchResults(count, results)
Example #16
0
def delete(group_id):
    """
    This function will remove a group from the database.
    """
    session = meta.Session()
    group = get(group_id)
    try:
        session.delete(group)
        session.flush()
    except:
        log.exception("Error removing group: {0}".format(group_id))
        raise
    
    return group
Example #17
0
def remove_old_session_files():
    cutoff = int(time.time() - 60 * config["sessions.timeout"])
    cutoff_dt = datetime.fromtimestamp(cutoff)
    log.debug("Checking for sessions older than {0}".format(cutoff_dt.strftime("%H:%M:%S")))
    for fname in os.listdir(config["sessions.storage_path"]):
        if not fname.startswith("."):
            try:
                fpath = os.path.join(config["sessions.storage_path"], fname)
                if os.stat(fpath).st_atime < cutoff:
                    log.debug("Removing stale session: {0}".format(fpath))
                    os.remove(fpath)
            except:
                log.exception("Error removign session: {0}".format(fname))
                pass
Example #18
0
 def authenticate(self, username, password):
     """
     This function will check the password passed in for the given
     userid against the database, and raise an exception if the authentication
     failse.
     
     :raise :class:`ensconce.exc.InvalidCredentials`: If username and/or password are invalid.
     """
     try:
         user = operators.get_by_username(username)
         if not pwhash.compare(user.password, password):
             raise exc.InvalidCredentials()
     except exc.NoSuchEntity:
         log.exception("No matching user.")
         raise exc.InvalidCredentials()
Example #19
0
def search(searchstr=None, order_by=None, offset=None, limit=None): # @ReservedAssignment
    """
    Search within resources and return matched results for specified limit/offset.
    
    :param searchstr: A search string that will be matched against name, addr, and description attributes.
    :type searchstr: str
    
    :param order_by: The sort column can be expressed as a string that includes asc/desc (e.g. "name asc").
    :type order_by: str
    
    :param offset: Offset in list for rows to return (supporting pagination).
    :type offset: int
    
    :param limit: Max rows to return (supporting pagination).
    :type limit: int
    
    :returns: A :class:`ensconce.dao.SearchResults` named tuple that includes count and list of :class:`ensconce.model.Resource` matches.
    :rtype: :class:`ensconce.dao.SearchResults`
    """
    session = meta.Session()
    try:
        r_t = model.resources_table
        
        if order_by is None:
            order_by = r_t.c.name
        
        clauses = []
        
        if searchstr:
            clauses.append(or_(r_t.c.name.ilike('%'+searchstr+'%'),
                             r_t.c.addr.ilike('%'+searchstr+'%'),
                             r_t.c.description.ilike('%'+searchstr+'%')))
        
        # (Well, there's only a single clause right now, so that's a little over-engineered)
        
        count = session.query(func.count(r_t.c.id)).filter(and_(*clauses)).scalar()
        
        q = session.query(model.Resource).filter(and_(*clauses)).order_by(order_by)
        
        if limit is not None:
            q = q.limit(limit)
        if offset is not None:
            q = q.offset(offset)
        
        return SearchResults(count=count, entries=q.all())
    except:
        log.exception("Error listing resources")
        raise
Example #20
0
def validate_passphrase(form, field):
    """
    """
    try:
        passphrase = field.data
        key = crypto_util.derive_configured_key(passphrase)
        if not crypto_util.validate_key(key):
            raise ValidationError("Invalid passphrase entered.")
    except ValidationError:
        raise
    except exc.MissingKeyMetadata:
        log.exception("Missing key metadata.")
        raise ValidationError("Database crypto has not yet been initialized.") 
    except:
        log.exception("Error validating passphrase.")
        raise ValidationError("Error validating passphrase (see server logs).")
Example #21
0
def create(name):
    """
    This function will create a group, add it to the database, and
    return the group_id of the newly created group.
    """
    session = meta.Session()
    try:
        group = model.Group()
        group.name = name
        session.add(group)
        session.flush()    
    except:
        log.exception("Error creating group: {0}".format(name))
        raise
    
    return group
Example #22
0
def list(): # @ReservedAssignment
    """
    This function will query the database, and return a list of all
    of the defined groups.

    Returns a list of [group_id, group_name] lists, one for each group
    defined in the database.
    """
    session = meta.Session()
    try:
        gt = model.groups_table
        groups = session.query(model.Group).order_by(gt.c.name).all()
    except:
        log.exception("Error retrieving groups.")
        raise
    else:
        return groups
Example #23
0
def get_by_username(username, assert_exists=True):
    """
    This function will attempt to match an operator by username.
    :param assert_exists: Whether to raise :class:`exc.exception if entity does not exist (avoid NPE later).
    """
    session = meta.Session()
    try:
        operator = session.query(model.Operator).filter_by(username=username).first()
    except:
        # The user ID didn't exist.
        log.exception("Unable to retrieve user for username: {0}".format(username))
        raise

    if assert_exists and not operator:
        raise exc.NoSuchEntity(model.Operator, username)

    return operator
Example #24
0
 def wrapper(*args, **kwargs):
     session = meta.Session()
     if not session.is_active:
         session.begin() # In case autocommit=True
     try:
         res = f(*args, **kwargs)
     except cherrypy.HTTPRedirect:
         # This is not a "real" exception, so we still want to commit the transaction.
         session.commit()
         raise
     except:
         log.exception("Rolling back SQLAlchemy transaction due to exception.")
         session.rollback()
         raise
     else:
         session.commit()
         return res
Example #25
0
def delete(access_id):
    """
    This will delete an access level.
    """
    session = meta.Session()
    try:
        alevel = session.query(model.Access).get(access_id)
        session.delete(alevel)
        session.flush()
    except IntegrityError:
        log.exception("Error deleting ACLs for access_id: {0}".format(access_id))
        raise exc.DataIntegrityError("Cannot delete in-use access level.")
    except:
        log.exception("Error deleting ACLs for access_id: {0}".format(access_id))
        raise
    
    return alevel
Example #26
0
def clear_key_metadata():
    """
    This is a utility function (built for testing) that just removes any key_metadata
    rows (with the intent that they will get re-created during crypto initialization phase).
    
    :raise ensconce.exc.UnconfiguredModel: If we can't create an SA session.
    """
    if meta.Session is None:
        raise exc.UnconfiguredModel()

    session = meta.Session()
    try:
        session.execute(model.key_metadata_table.delete())
        session.commit()  # We are deliberately committing early here
    except:
        session.rollback()
        log.exception("Error clearing key metadata table.")
        raise
Example #27
0
def remove_old_backups():
    # Convert config value of 'days' into 'seconds'
    cutoff = int(time.time() - (60 * 60 * 24) * config["backups.remove_older_than_days"])
    cutoff_dt = datetime.fromtimestamp(cutoff)
    if os.path.exists(config["backups.path"]):
        log.debug("Checking for backups older than {0}".format(cutoff_dt.strftime("%m/%d %H:%M:%S")))
        for fname in os.listdir(config["backups.path"]):
            if not fname.startswith("."):
                try:
                    fpath = os.path.join(config["backups.path"], fname)
                    if os.stat(fpath).st_mtime < cutoff:
                        log.debug("Removing old backup: {0}".format(fpath))
                        os.remove(fpath)
                except:
                    log.exception("Error removing old backup file: {0}".format(fname))
                    pass
    else:
        log.error("Unable to remove old backups; backup path does not exist: {0}".format(config["backups.path"]))
Example #28
0
def get(access_id, assert_exists=True):
    """
    This function will return an Access class for a given access_id or None if it does not exist.
    :param password_id: The ID for operator to lookup.
    :param assert_exists: Whether to raise exception if entity does not exist (avoid NPE later).
    :rtype: :class:`model.Operator`
    """
    session = meta.Session()
    try:
        alevel = session.query(model.Access).get(access_id)
    except:
        log.exception("Unable to get access results for access_id: {0}".format(access_id))
        raise
    
    if assert_exists and not alevel:
        raise exc.NoSuchEntity(model.Resource, access_id)
    
    return alevel
Example #29
0
def modify(group_id, **kwargs):
    """
    This function will update a group record in the database.
    
    :keyword name: The group name.
    :raise ValueError: If the group ID cannot be resolved.
    """
    session = meta.Session()
    group = get(group_id)
    update_attributes = kwargs
    try:
        modified = model.set_entity_attributes(group, update_attributes)
        session.flush()
    except:
        log.exception("Error updating group: {0}".format(group_id))
        raise
    
    return (group, modified)
Example #30
0
def notify(message):
    """
    Pushes a notification messages onto the user's session.
    :param message: str
    """
    try:
        if isinstance(message, unicode):
            message = message.encode('utf8')
        try:
            notifications = cherrypy.session['notifications'] # @UndefinedVariable
        except KeyError:
            cherrypy.session['notifications'] = notifications = [] # @UndefinedVariable
            
        notifications.append(message)
    except:
        # Do *not* make notification rollback our transaction.
        log.exception("Unable to add notification.")
        pass