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 _create_resources(self): session = meta.Session() R = collections.namedtuple('Resource', ['name', 'addr', 'description', 'notes', 'tags', 'groups']) data = (R('host1.example.com', '192.168.1.1', None, 'Encrypted notes', [], ['First Group', 'Second Group']), R('host2.example', '192.168.1.2', None, None, ['tagonly'], ['First Group']), R('Bikeshed PIN', None, None, 'L-R-L-R-U-D-U-D 123', ['tagprefix:tagone'], ['First Group']), R('BoA', '172.16.20.39', 'Online bank', 'The bank picture should be a monkey!', ['tagone', 'tagtwo'], ['First Group', 'Third Group']), R(u'faß.de', '10.0.1.1', u'ვეპხის ტყაოსანი შოთა რუსთაველი', u"Sîne klâwen durh die wolken sint geslagen", [], ['6th group', 'First Group']), R(u'ԛәлп.com', '10.0.1.2', u'ಬಾ ಇಲ್ಲಿ ಸಂಭವಿಸು ಇಂದೆನ್ನ ಹೃದಯದಲಿ', u"На берегу пустынных волн", [], ['6th group', 'First Group']), R('https-server', '127.0.0.1', 'Testing SSL cert/key storage.', None, ['tagtwo', 'tagone'], ['Fourth Group']), ) for el in data: r = model.Resource() r.name = el.name r.addr = el.addr r.description = el.description r.notes_decrypted = el.notes if el.tags: r.tags = ' '.join(el.tags) for gname in el.groups: r.groups.append(self.groups[gname]) session.add(r) self.resources[r.name] = r session.flush() log.debug("Created {0}".format(r))
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 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 _create_operators(self): session = meta.Session() for username, pw in (('op1', 'pw1'), ('op2', 'pw2'), ('op3', 'pw3')): o = model.Operator() o.username = username o.password = pwhash.obscure(pw) o.access_id = 1 # FIXME: Probably should be creating access levels in data pouplator session.add(o) self.operators[o.username] = o session.flush() log.debug("Created {0}".format(o))
def _create_groups(self): session = meta.Session() for name in ('First Group', 'Second Group', 'Third Group', 'Fourth Group', 'fifth group', '6th group'): g = model.Group() g.name = name g.date_created = datetime.now(tz=pytz.utc) g.creator = self.operators['op1'] session.add(g) self.groups[name] = g session.flush() log.debug("Created {0}".format(g))
def _create_passwords(self): session = meta.Session() r = self.resources['host1.example.com'] for i in range(5): p = model.Password() p.username = '******'.format(i) p.password_decrypted = 'password{0}'.format(i) p.description = 'Description Text' r.passwords.append(p) session.flush() log.debug("Added {0} to {1}".format(p, r)) r = self.resources['host2.example'] for i,un in enumerate(('user', 'root')): p = model.Password() p.username = un p.password_decrypted = 'AxF$#( )#-{0}'.format(i) p.description = None r.passwords.append(p) session.flush() log.debug("Added {0} to {1}".format(p, r)) r = self.resources['BoA'] p = model.Password() p.username = '******' p.password_decrypted = '!MonkeyPass!'.format(i) p.description = 'Remember, picture should be a monkey. Woah, this is not encrypted?' p.tags = 'tagprefix2:tagone tagtwo' r.passwords.append(p) session.flush() log.debug("Added {0} to {1}".format(p, r)) r = self.resources['https-server'] p = model.Password() p.username = '******' p.password_decrypted = sslsamples.keypair1.cert # @UndefinedVariable p.description = 'PEM-encoded x509 certificate' p.tags = 'tagtwo tagprefix:tagone' r.passwords.append(p) session.flush() log.debug("Added {0} to {1}".format(p, r)) r = self.resources['https-server'] p = model.Password() p.username = '******' p.password_decrypted = sslsamples.keypair1.key # @UndefinedVariable p.description = 'PEM-encoded 2048-bit RSA key' p.tags = 'tagprefix' r.passwords.append(p) session.flush() log.debug("Added {0} to {1}".format(p, r))
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 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 process_edit(self, **kwargs): log.debug("params = %r" % request_params()) form = OperatorEditForm(request_params()) form.access_id.choices = [(l.id, l.description) for l in access.list()] if form.validate(): params = dict(operator_id=form.operator_id.data, username=form.username.data, access_id=form.access_id.data) # If password is blank, let's just not change it. if form.password.data: params['password'] = form.password.data (operator, modified) = operators.modify(**params) auditlog.log(auditlog.CODE_CONTENT_MOD, target=operator, attributes_modified=modified) notify_entity_activity(operator, 'updated') raise cherrypy.HTTPRedirect('/user/list') else: return render('user/edit.html', {'form': form, 'externally_managed': operator.externally_managed})
def auditlog(self, **kwargs): form = AuditlogForm(request_params()) page_size = 50 page = form.page.data offset = page_size * (page - 1) limit = page_size log.debug("Page = {0}, offset={1}, limit={2}".format(page, offset, limit)) results = auditlog.search(start=form.start.data, end=form.end.data, code=form.code.data, operator_username=form.operator.data, offset=offset, limit=limit) if results.count < offset: form.page.data = 1 form.page.raw_data = ['1'] # Apparently need this too! total_pages = int(math.ceil( (1.0 * results.count) / page_size)) return render('auditlog.html', {'entries': results.entries, 'form': form, 'total_pages': total_pages})
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 tearDownClass(cls): if hasattr(cls, 'wd'): log.debug("Quittting the WebDriver.") cls.wd.quit() super(SeleniumTestController, cls).tearDownClass()
def get_webdriver(cls): exe = '{0}/wd/hub'.format(cls.driver.remote_host) cap = getattr(DesiredCapabilities, cls.driver.capabilities) log.debug("{0!r} {1!r}", exe, cap) return webdriver.Remote(command_executor=exe, desired_capabilities=cap)
def start(self): log.debug("Re-initializing the RNG after any forking.") Random.atfork()
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
def search(start=None, end=None, operator_id=None, operator_username=None, code=None, object_type=None, object_id=None, offset=None, limit=None, skip_count=False): session = meta.Session() try: a_t = model.auditlog_table q = session.query(model.AuditlogEntry) clauses = [] if start: applog.debug("Filtering on start date: {0}".format(start)) clauses.append(a_t.c.datetime >= start) if end: applog.debug("Filtering on end date: {0}".format(end)) clauses.append(a_t.c.datetime <= end) if operator_id: if operator_username: warnings.warn("Ignoring operator_username parameter, since operator_id was specified.") clauses.append(a_t.c.operator_id == operator_id) elif operator_username: applog.debug("Filtering on username: {0}".format(operator_username)) clauses.append(a_t.c.operator_username == operator_username) if object_type: applog.debug("Filtering on object type: {0}".format(object_type)) clauses.append(a_t.c.object_type == object_type) if object_id: applog.debug("Filtering on object id: {0}".format(object_id)) clauses.append(a_t.c.object_type == object_id) if code: applog.debug("Filtering on code: {0}".format(code)) clauses.append(a_t.c.code.like(code)) # Allow for code wildcards (e.g. "content.%" to be passed in if not skip_count: count = session.query(func.count(a_t.c.id)).filter(and_(*clauses)).scalar() else: count = None applog.debug("Total number of rows: {0}".format(count)) q = q.filter(and_(*clauses)) q = q.order_by(a_t.c.datetime.desc()) if offset and count > offset: q = q.offset(offset) if limit: q = q.limit(limit) #applog.debug("Auditlog query: {0}".format(q)) results = q.all() except: applog.exception("Error searching audit log.") raise return SearchResults(count, results)
def edit(self, resource_id): resource = resources.get(resource_id) log.debug("Resource matched: {0!r}".format(resource)) form = ResourceEditForm(request_params(), obj=resource, resource_id=resource_id, group_ids=[g.id for g in resource.groups]) form.group_ids.choices = [(g.id, g.label) for g in groups.list()] return render('resource/edit.html', {'form': form})
def tagsearch(tags, search_resources=True, search_passwords=True): """ This function will search the database for all occurances of specified tags (AND) in the resources and passwords fields. :param tags: The list of tags to search for. :type tags: list :param search_resources: Whether to search the resources table.. :param search_passwords: Whether to search the passwords table. :returns: A tuple of (resource matches, password matches) :rtype: :class:`ensconce.search.TagSearchResults` """ if isinstance(tags, basestring): tags = [tags] session = meta.Session() resource_results = [] password_results = [] def _process_tag(tag): tag = tag.strip() # Support prefixing or suffixing tags with ':' prefix = tag.startswith(':') suffix = tag.endswith(':') if prefix or suffix: tag = tag.strip(':') tag = re.escape(tag) if prefix: tag = '.+?\:' + tag if suffix: tag += '\:.+?' # Defnitely possible here to create something that won't match anything return tag if search_resources: r_t = model.resources_table try: r_clause = [] for tag in tags: tagstr = _process_tag(tag) r_clause.append(r_t.c.tags.op('~*')(r'([^|[:alnum:]_-]|^){0}([^[:alnum:]_-]|$)'.format(tagstr))) q = session.query(model.Resource).filter(and_(*r_clause)) q = q.order_by(r_t.c.name) resource_results = q.all() log.debug("Got these resource results: {0!r}".format(resource_results)) except: log.exception("Error searching on resources.") raise if search_passwords: try: p_t = model.passwords_table p_clause = [] for tag in tags: tagstr = _process_tag(tag) p_clause.append(p_t.c.tags.op('~*')(r'([^|[:alnum:]_-]|^){0}([^[:alnum:]_-]|$)'.format(tagstr))) # And these are the users/passwords associated with resources q = session.query(model.Password).filter(and_(*p_clause)) q = q.order_by(p_t.c.username) password_results = q.all() log.debug("Got these password results: {0!r}".format(password_results)) except: log.exception("Error searching on passwords.") raise return TagSearchResults(resource_results, password_results)
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