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 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
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
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
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 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 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
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)
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 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)
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
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)
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
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
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)
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
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
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()
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
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).")
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
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
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
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
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
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
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"]))
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
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)
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