def trace_permissions(): embed = request.args.get('embed', '') user_id = request.args.get('user', '') folder_path = request.args.get('path', '') if folder_path == '': folder_path = os.path.sep folder = None user = None users = [] user_has_admin = False trace = None err_msg = None db_session = data_engine.db_get_session() try: # Get folder and selected user info # User can be None for an anonymous user user_id = parse_int(user_id) if user_id != 0: user = data_engine.get_user(user_id, _db_session=db_session) if user is None: raise DoesNotExistError('This user no longer exists') folder = data_engine.get_folder(folder_path=folder_path, _db_session=db_session) if folder is None or folder.status == Folder.STATUS_DELETED: raise DoesNotExistError('This folder no longer exists') # Get users list users = data_engine.list_users(status=User.STATUS_ACTIVE, order_field=User.username, _db_session=db_session) # Get the folder+user traced permissions trace = permissions_engine._trace_folder_permissions(folder, user) # Flag on the UI if the user has admin for gdict in trace['groups']: gperms = gdict['group'].permissions if gperms.admin_files or gperms.admin_all: user_has_admin = True break except Exception as e: log_security_error(e, request) err_msg = safe_error_str(e) finally: try: return render_template( 'admin_trace_permissions.html', embed=embed, folder=folder, folder_is_root=folder.is_root() if folder else False, user=user, user_list=users, trace=trace, user_has_admin=user_has_admin, err_msg=err_msg, GROUP_ID_PUBLIC=Group.ID_PUBLIC) finally: db_session.close()
def get(self, folder_id=None): """ Gets a folder by path or ID, returning 1 level of children (sub-folders) """ if folder_id is None: # Get folder from path, using auto_sync to pick up new and deleted disk folders path = self._get_validated_path_arg(request) db_folder = auto_sync_folder(path, data_engine, task_engine) if db_folder is None: raise DoesNotExistError(path) else: # Get folder from ID db_folder = data_engine.get_folder(folder_id) if db_folder is None: raise DoesNotExistError(str(folder_id)) # View permission is required (ignoring view permission on parent+children) permissions_engine.ensure_folder_permitted( db_folder, FolderPermission.ACCESS_VIEW, get_session_user()) # Get the folder again, this time with parent and children # (children possibly faked - see the get_folder() docs - which is why # we can't use db_folder mk2 normally, only serialize it and exit) status_filter = self._get_validated_status_arg(request) db_folder = data_engine.get_folder(db_folder.id, load_parent=True, load_children=True, children_status=status_filter) if db_folder is None: raise DoesNotExistError(str(folder_id)) return make_api_success_response(object_to_dict(db_folder))
def get(self, folio_id, export_id=None): # Get the portfolio folio = data_engine.get_portfolio(folio_id) if folio is None: raise DoesNotExistError(str(folio_id)) # Check permissions permissions_engine.ensure_portfolio_permitted( folio, FolioPermission.ACCESS_VIEW, get_session_user()) if export_id is None: # List portfolio exports exports_list = [ _prep_folioexport_object(folio, fe) for fe in folio.downloads ] return make_api_success_response( object_to_dict_list(exports_list, _omit_fields)) else: # Get a single portfolio-export folio_export = data_engine.get_object(FolioExport, export_id) if folio_export is None: raise DoesNotExistError(str(export_id)) if folio_export.folio_id != folio_id: raise ParameterError( 'export ID %d does not belong to portfolio ID %d' % (export_id, folio_id)) return make_api_success_response( object_to_dict(_prep_folioexport_object(folio, folio_export), _omit_fields))
def get(self, folio_id, image_id=None): if image_id is None: # List images in the portfolio folio = data_engine.get_portfolio(folio_id, load_images=True) if folio is None: raise DoesNotExistError(str(folio_id)) # Check permissions permissions_engine.ensure_portfolio_permitted( folio, FolioPermission.ACCESS_VIEW, get_session_user()) image_list = [_prep_folioimage_object(fi) for fi in folio.images] return make_api_success_response( object_to_dict_list(image_list, _omit_fields)) else: # Get a single portfolio-image db_session = data_engine.db_get_session() try: folio_image = data_engine.get_portfolio_image( AttrObject(id=folio_id), AttrObject(id=image_id), _db_session=db_session) if folio_image is None: raise DoesNotExistError( str(folio_id) + '/' + str(image_id)) # Check permissions permissions_engine.ensure_portfolio_permitted( folio_image.portfolio, FolioPermission.ACCESS_VIEW, get_session_user()) return make_api_success_response( object_to_dict(_prep_folioimage_object(folio_image), _omit_fields + ['portfolio'])) finally: db_session.close()
def post(self): params = self._get_validated_object_parameters(request.form) db_session = data_engine.db_get_session() db_commit = False try: db_group = data_engine.get_group(params['group_id'], _db_session=db_session) if db_group is None: raise DoesNotExistError(str(params['group_id'])) db_folder = data_engine.get_folder(params['folder_id'], _db_session=db_session) if db_folder is None: raise DoesNotExistError(str(params['folder_id'])) # This commits (needed for refresh to get the new ID) fp = FolderPermission(db_folder, db_group, params['access']) fp = data_engine.save_object(fp, refresh=True, _db_session=db_session, _commit=True) db_commit = True return make_api_success_response(object_to_dict(fp)) finally: try: if db_commit: db_session.commit() permissions_engine.reset_folder_permissions() else: db_session.rollback() finally: db_session.close()
def delete(self, folio_id, export_id): db_session = data_engine.db_get_session() try: # Get the portfolio folio = data_engine.get_portfolio(folio_id, _db_session=db_session) if folio is None: raise DoesNotExistError(str(folio_id)) # Check permissions permissions_engine.ensure_portfolio_permitted( folio, FolioPermission.ACCESS_EDIT, get_session_user()) # Get the single portfolio-export folio_export = data_engine.get_object(FolioExport, export_id, _db_session=db_session) if folio_export is None: raise DoesNotExistError(str(export_id)) if folio_export.folio_id != folio_id: raise ParameterError( 'export ID %d does not belong to portfolio ID %d' % (export_id, folio_id)) # Delete it and the export files delete_portfolio_export(folio_export, get_session_user(), 'Deleted: ' + folio_export.describe(True), _db_session=db_session) return make_api_success_response() finally: db_session.close()
def portfolio_download(human_id, filename): logger.debug('GET ' + request.url) try: # Find the portfolio folio = data_engine.get_portfolio(human_id=human_id) if not folio: raise DoesNotExistError('Portfolio \'%s\' does not exist' % human_id) # Ensure that the user has permission to download the portfolio user = get_session_user() permissions_engine.ensure_portfolio_permitted( folio, FolioPermission.ACCESS_DOWNLOAD, user) # Check that the filename is valid (note: assumes folio.downloads is eager loaded) if not filename: raise DoesNotExistError('No filename specified') folio_exports = [ dl for dl in folio.downloads if dl.filename == filename ] if not folio_exports: raise DoesNotExistError('Download \'%s\' is not available' % filename) folio_export = folio_exports[0] # The physical file should always exist when the data+filename exists # This also checks that the file path lies inside IMAGES_BASE_DIR zip_path = get_portfolio_export_file_path(folio_export) ensure_path_exists(zip_path, require_file=True) # Prepare to serve the file response = send_file( get_abs_path(zip_path), mimetype='application/zip', as_attachment=True, conditional=True, cache_timeout=31536000 # zips never change once created ) # Lastly write an audit record data_engine.add_portfolio_history(folio, user, FolioHistory.ACTION_DOWNLOADED, folio_export.filename) return response except httpexc.HTTPException: # Pass through HTTP 4xx and 5xx raise except SecurityError as e: if app.config['DEBUG']: raise log_security_error(e, request) raise httpexc.Forbidden() except DoesNotExistError as e: logger.warning('404 Not found: ' + str(e)) raise httpexc.NotFound(safe_error_str(e)) except Exception as e: if app.config['DEBUG']: raise logger.error('500 Error for ' + request.url + '\n' + str(e)) raise httpexc.InternalServerError(safe_error_str(e))
def folder_permissions(): folder_path = request.args.get('path', '') if folder_path == '': folder_path = os.path.sep group_id = request.args.get('group', '') if group_id == '': group_id = Group.ID_PUBLIC group = None folder = None current_perms = None groups = [] err_msg = None db_session = data_engine.db_get_session() try: # Get folder and group info group = data_engine.get_group(group_id, _db_session=db_session) if group is None: raise DoesNotExistError('This group no longer exists') folder = data_engine.get_folder(folder_path=folder_path, _db_session=db_session) if folder is None or folder.status == Folder.STATUS_DELETED: raise DoesNotExistError('This folder no longer exists') # Get groups list groups = data_engine.list_objects(Group, Group.name, _db_session=db_session) # Get the current permissions for the folder+group, which can be None. # Note that permissions_manager might fall back to the Public group if # this is None, but to keep the admin manageable we're going to deal # only with folder inheritance, not group inheritance too. current_perms = data_engine.get_nearest_folder_permission( folder, group, _load_nearest_folder=True, _db_session=db_session) except Exception as e: log_security_error(e, request) err_msg = safe_error_str(e) finally: try: return render_template( 'admin_folder_permissions.html', group=group, folder=folder, folder_is_root=folder.is_root() if folder else False, current_permissions=current_perms, group_list=groups, err_msg=err_msg, GROUP_ID_PUBLIC=Group.ID_PUBLIC, GROUP_ID_EVERYONE=Group.ID_EVERYONE) finally: db_session.close()
def post(self, folio_id): db_session = data_engine.db_get_session() try: # Get the portfolio folio = data_engine.get_portfolio(folio_id, _db_session=db_session) if folio is None: raise DoesNotExistError(str(folio_id)) # Check portfolio permissions permissions_engine.ensure_portfolio_permitted( folio, FolioPermission.ACCESS_EDIT, get_session_user()) # Get the image by either ID or src params = self._get_validated_object_parameters(request.form, True) if 'image_id' in params: image = data_engine.get_image(params['image_id'], _db_session=db_session) if image is None: raise DoesNotExistError(str(params['image_id'])) else: image = auto_sync_file(params['image_src'], data_engine, task_engine, anon_history=True, burst_pdf=False, _db_session=db_session) if image is None or image.status == Image.STATUS_DELETED: raise DoesNotExistError(params['image_src']) # Check image permissions permissions_engine.ensure_folder_permitted( image.folder, FolderPermission.ACCESS_VIEW, get_session_user(), False) # Add history first so that we only commit once at the end data_engine.add_portfolio_history(folio, get_session_user(), FolioHistory.ACTION_IMAGE_CHANGE, '%s added' % image.src, _db_session=db_session, _commit=False) # Flag that exported zips are now out of date folio.last_updated = datetime.utcnow() # Add the image and commit changes db_folio_image = data_engine.save_object(FolioImage( folio, image, params['image_parameters'], params['filename'], params['index']), refresh=True, _db_session=db_session, _commit=True) return make_api_success_response( object_to_dict(_prep_folioimage_object(db_folio_image), _omit_fields + ['portfolio'])) finally: db_session.close()
def _set_permissions(self, folio, params, db_session): """ Sets portfolio permissions from the params and returns whether the previous permissions were changed. """ changed = False param_names = { Group.ID_PUBLIC: 'public_access', Group.ID_EVERYONE: 'internal_access' } for group_id in param_names: access_level = params[param_names[group_id]] current_perm = [ fp for fp in folio.permissions if fp.group_id == group_id ] if not current_perm: # Add missing folder permission for group db_group = data_engine.get_group(group_id, _db_session=db_session) if db_group is None: raise DoesNotExistError(param_names[group_id] + ' group') changed = True folio.permissions.append( FolioPermission(folio, db_group, access_level)) else: # Update the existing folder permission for group fp = current_perm[0] changed = changed or (fp.access != access_level) fp.access = access_level return changed
def delete(self, permission_id): db_session = data_engine.db_get_session() db_commit = False try: fp = data_engine.get_object(FolderPermission, permission_id, _db_session=db_session) if fp is None: raise DoesNotExistError(str(permission_id)) try: data_engine.delete_folder_permission(fp, _db_session=db_session, _commit=False) except ValueError as e: raise ParameterError(str(e)) db_commit = True return make_api_success_response() finally: if db_commit: db_session.commit() permissions_engine.reset_folder_permissions() else: db_session.rollback() db_session.close()
def post(self, group_id): params = self._get_validated_object_parameters(request.form) group = data_engine.get_group(group_id=group_id, load_users=True) if group is None: raise DoesNotExistError(str(group_id)) # Check permissions! The current user must have user admin to be here. # But if they don't also have permissions admin or superuser then we # must block the change if the new group would grant one of the same. if group.permissions.admin_permissions or group.permissions.admin_all: if not permissions_engine.is_permitted( SystemPermissions.PERMIT_ADMIN_PERMISSIONS, get_session_user()): raise SecurityError( 'You cannot add users to a group that ' + 'grants permissions administration, because you do not ' + 'have permissions administration access yourself.') user = data_engine.get_user(user_id=params['user_id']) if user is not None: if user not in group.users: group.users.append(user) data_engine.save_object(group) reset_user_sessions(user) permissions_engine.reset() return make_api_success_response()
def delete(self, folio_id, image_id): db_session = data_engine.db_get_session() try: folio_image = data_engine.get_portfolio_image( AttrObject(id=folio_id), AttrObject(id=image_id), _db_session=db_session) if folio_image is None: raise DoesNotExistError(str(folio_id) + '/' + str(image_id)) # Check permissions permissions_engine.ensure_portfolio_permitted( folio_image.portfolio, FolioPermission.ACCESS_EDIT, get_session_user()) # Add history first so that we only commit once at the end folio = folio_image.portfolio data_engine.add_portfolio_history(folio, get_session_user(), FolioHistory.ACTION_IMAGE_CHANGE, '%s removed' % folio_image.image.src, _db_session=db_session, _commit=False) # Flag that exported zips will be out of date folio.last_updated = datetime.utcnow() # Delete the image from the portfolio and commit changes data_engine.delete_object(folio_image, _db_session=db_session, _commit=True) return make_api_success_response() finally: db_session.close()
def imagedetails(): # Get/check parameters try: src = request.args.get('src', '') validate_string(src, 1, 1024) except ValueError as e: raise ParameterError(e) # v2.6.4 Don't allow this call to populate the database with unsupported files supported_file = ( get_file_extension(src) in image_engine.get_image_formats(supported_only=True) ) if not supported_file and path_exists(src, require_file=True): raise ImageError('The file is not a supported image format') # Get the image database entry db_image = auto_sync_file(src, data_engine, task_engine) if not db_image or db_image.status == Image.STATUS_DELETED: raise DoesNotExistError(src) # Require view permission or file admin permissions_engine.ensure_folder_permitted( db_image.folder, FolderPermission.ACCESS_VIEW, get_session_user() ) return make_api_success_response(object_to_dict( _prep_image_object(db_image), _omit_fields ))
def put(self, permission_id): params = self._get_validated_object_parameters(request.form) fp = data_engine.get_object(FolderPermission, permission_id) if fp is None: raise DoesNotExistError(str(permission_id)) fp.access = params['access'] data_engine.save_object(fp) permissions_engine.reset_folder_permissions() return make_api_success_response(object_to_dict(fp))
def put(self, property_id): params = self._get_validated_object_parameters(request.form) db_prop = data_engine.get_object(Property, property_id) if db_prop is None: raise DoesNotExistError(str(property_id)) db_prop.value = params['value'] data_engine.save_object(db_prop) if property_id == Property.DEFAULT_TEMPLATE: image_engine.reset_templates() return make_api_success_response(object_to_dict(db_prop))
def delete(self, folder_id): """ Deletes a disk folder """ # v4.1 #10 delete_folder() doesn't care whether it exists, but we want the # API to return a "not found" if the folder doesn't exist on disk # (and as long as the database is already in sync with that) db_folder = data_engine.get_folder(folder_id) if db_folder is None: raise DoesNotExistError(str(folder_id)) if not path_exists(db_folder.path, require_directory=True ) and db_folder.status == Folder.STATUS_DELETED: raise DoesNotExistError(db_folder.path) # Run this as a background task in case it takes a long time task = task_engine.add_task(get_session_user(), 'Delete disk folder %d' % folder_id, 'delete_folder', {'folder_id': folder_id}, Task.PRIORITY_HIGH, 'info', 'error', 10) if task is None: # Task already submitted return make_api_success_response(task_accepted=True) else: return self._task_response(task, 30)
def get(self, permission_id=None): if permission_id is None: # List all permissions fp_list = data_engine.list_objects(FolderPermission) return make_api_success_response(object_to_dict_list(fp_list)) else: # Get permission entry fp = data_engine.get_object(FolderPermission, permission_id) if fp is None: raise DoesNotExistError(str(permission_id)) return make_api_success_response(object_to_dict(fp))
def get(self, image_id): db_img = data_engine.get_image(image_id=image_id) if not db_img: raise DoesNotExistError(str(image_id)) else: # Require view permission or file admin permissions_engine.ensure_folder_permitted( db_img.folder, FolderPermission.ACCESS_VIEW, get_session_user()) return make_api_success_response( object_to_dict(_prep_image_object(db_img)))
def get(self, group_id=None): if group_id is None: # List groups return make_api_success_response( object_to_dict_list(data_engine.list_objects( Group, Group.name))) else: # Get single group group = data_engine.get_group(group_id=group_id, load_users=True) if group is None: raise DoesNotExistError(str(group_id)) return make_api_success_response(object_to_dict(group))
def delete(self, image_id): """ Deletes a file from disk """ # Get image data db_img = data_engine.get_image(image_id=image_id) if not db_img: raise DoesNotExistError(str(image_id)) # v4.1 #10 delete_file() doesn't care whether the file exists, but we # want the API to return a "not found" if the file doesn't exist # (and as long as the database is already in sync with that) if not path_exists( db_img.src, require_file=True) and db_img.status == Image.STATUS_DELETED: raise DoesNotExistError(db_img.src) # Delete db_img = delete_file(db_img, get_session_user(), data_engine, permissions_engine) # Remove cached images for old path image_engine._uncache_image_id(db_img.id) # Return updated image return make_api_success_response( object_to_dict(_prep_image_object(db_img)))
def put(self, image_id): params = self._get_validated_object_parameters(request.form) # Get image and update it db_img = data_engine.get_image(image_id=image_id) if not db_img: raise DoesNotExistError(str(image_id)) # Require edit permission or file admin permissions_engine.ensure_folder_permitted( db_img.folder, FolderPermission.ACCESS_EDIT, get_session_user()) old_title = db_img.title old_description = db_img.description db_img.title = params['title'] db_img.description = params['description'] data_engine.save_object(db_img) # Get text changes. Max info length = # 100 + 200 + len('()' + '()' + 'Title: ' + ' / ' + 'Description: ') ==> 327 title_diff = get_string_changes(old_title, params['title'], char_limit=100).strip() if not title_diff: # Try for deletions from title title_diff = get_string_changes(params['title'], old_title, char_limit=100).strip() if title_diff: title_diff = '(' + title_diff + ')' desc_diff = get_string_changes(old_description, params['description'], char_limit=200).strip() if not desc_diff: # Try for deletions from description desc_diff = get_string_changes(params['description'], old_description, char_limit=200).strip() if desc_diff: desc_diff = '(' + desc_diff + ')' info = '' if title_diff: info += 'Title: ' + title_diff if info and desc_diff: info += ' / ' if desc_diff: info += 'Description: ' + desc_diff # Add change history data_engine.add_image_history(db_img, get_session_user(), ImageHistory.ACTION_EDITED, info) return make_api_success_response( object_to_dict(_prep_image_object(db_img)))
def get(self, user_id=None): if user_id is None: # List users status_filter = self._get_validated_status_arg(request) ulist = data_engine.list_users(status=status_filter, order_field=User.username) return make_api_success_response(object_to_dict_list(ulist)) else: # Get single user user = data_engine.get_user(user_id) if user is None: raise DoesNotExistError(str(user_id)) return make_api_success_response(object_to_dict(user))
def delete(self, user_id): user = data_engine.get_user(user_id=user_id) if user is None: raise DoesNotExistError(str(user_id)) if user.id == 1: raise ParameterError('The \'admin\' user cannot be deleted') data_engine.delete_user(user) # If this is the current user, log out if get_session_user_id() == user_id: log_out() # Reset session caches reset_user_sessions(user) return make_api_success_response(object_to_dict(user))
def put(self, template_id): permissions_engine.ensure_permitted( SystemPermissions.PERMIT_SUPER_USER, get_session_user()) params = self._get_validated_object_parameters(request.form) template = data_engine.get_image_template(template_id) if template is None: raise DoesNotExistError(str(template_id)) template.name = params['name'] template.description = params['description'] template.template = params['template'] data_engine.save_object(template) image_engine.reset_templates() return self.get(template.id)
def delete(self, template_id): permissions_engine.ensure_permitted( SystemPermissions.PERMIT_SUPER_USER, get_session_user()) template_info = data_engine.get_image_template(template_id) if template_info is None: raise DoesNotExistError(str(template_id)) db_default_template = data_engine.get_object(Property, Property.DEFAULT_TEMPLATE) if template_info.name.lower() == db_default_template.value.lower(): raise ParameterError( 'The system default template cannot be deleted') data_engine.delete_object(template_info) image_engine.reset_templates() return make_api_success_response()
def put(self, folio_id): db_session = data_engine.db_get_session() try: # Get portfolio folio = data_engine.get_portfolio(folio_id, _db_session=db_session) if folio is None: raise DoesNotExistError(str(folio_id)) # Check permissions permissions_engine.ensure_portfolio_permitted( folio, FolioPermission.ACCESS_EDIT, get_session_user()) # Update the object params = self._get_validated_object_parameters(request.form) permissions_changed = self._set_permissions( folio, params, db_session) changes = [] if params['human_id'] != folio.human_id: changes.append('short URL changed') if params['name'] != folio.name: changes.append('name changed') if params['description'] != folio.description: changes.append('description changed') if permissions_changed: changes.append('permissions changed') folio.human_id = params['human_id'] or Folio.create_human_id() folio.name = params['name'] folio.description = params['description'] # Note: folio.last_updated is only for image changes # (to know when to invalidate the exported zips) data_engine.add_portfolio_history(folio, get_session_user(), FolioHistory.ACTION_EDITED, ', '.join(changes).capitalize(), _db_session=db_session, _commit=False) data_engine.save_object( folio, _db_session=db_session, _commit=True # fail here if human_id not unique ) if permissions_changed: permissions_engine.reset_portfolio_permissions() # Return a clean object the same as for get(id) folio = data_engine.get_portfolio(folio.id, load_images=True, load_history=True) folio = _prep_folio_object(folio) return make_api_success_response( object_to_dict(folio, _omit_fields)) finally: db_session.close()
def put(self, folio_id, image_id): db_session = data_engine.db_get_session() try: folio_image = data_engine.get_portfolio_image( AttrObject(id=folio_id), AttrObject(id=image_id), _db_session=db_session) if folio_image is None: raise DoesNotExistError(str(folio_id) + '/' + str(image_id)) # Check permissions permissions_engine.ensure_portfolio_permitted( folio_image.portfolio, FolioPermission.ACCESS_EDIT, get_session_user()) # Update the object with any/all parameters that were passed in params = self._get_validated_object_parameters(request.form, False) changes = [] affects_zips = False if (params['image_parameters'] is not None and params['image_parameters'] != folio_image.parameters): folio_image.parameters = params['image_parameters'] changes.append('image attributes changed') affects_zips = True if (params['filename'] is not None and params['filename'] != folio_image.filename): folio_image.filename = params['filename'] changes.append('filename changed') affects_zips = True if (params['index'] is not None and params['index'] != folio_image.order_num): folio_image.order_num = params['index'] changes.append('set as position %d' % (params['index'] + 1)) if changes: # Flag if exported zips will be out of date if affects_zips: folio_image.portfolio.last_updated = datetime.utcnow() # Add history and commit changes data_engine.add_portfolio_history( folio_image.portfolio, get_session_user(), FolioHistory.ACTION_IMAGE_CHANGE, '%s updated: %s' % (folio_image.image.src, ', '.join(changes)), _db_session=db_session, _commit=True) return make_api_success_response( object_to_dict(_prep_folioimage_object(folio_image), _omit_fields + ['portfolio'])) finally: db_session.close()
def delete(self, group_id): # Check permissions! The current user must have permissions admin to delete groups. permissions_engine.ensure_permitted( SystemPermissions.PERMIT_ADMIN_PERMISSIONS, get_session_user()) group = data_engine.get_group(group_id=group_id, load_users=True) if group is None: raise DoesNotExistError(str(group_id)) try: data_engine.delete_group(group) except ValueError as e: raise ParameterError(str(e)) # Reset permissions and session caches reset_user_sessions(group.users) permissions_engine.reset() return make_api_success_response()
def delete(self, group_id, user_id): group = data_engine.get_group(group_id=group_id, load_users=True) if group is None: raise DoesNotExistError(str(group_id)) # Back up the object in case we need to restore it backup_group = copy.deepcopy(group) # Update group membership for idx, member in enumerate(group.users): if member.id == user_id: del group.users[idx] data_engine.save_object(group) reset_user_sessions(member) permissions_engine.reset() _check_for_user_lockout(backup_group) break return make_api_success_response()