def _check_for_user_lockout(original_object): """ Only to be called when the current user is known to have PERMIT_ADMIN_USERS permission, checks that the current user hasn't locked themselves out from user administration. Also checks that the admin user's administration permission has not been accidentally revoked. If a lockout has occurred, the supplied original object is re-saved and a ParameterError is raised. """ user_ids = [get_session_user_id(), 1] for user_id in user_ids: db_user = data_engine.get_user(user_id=user_id) if db_user: try: # Require user administration if not permissions_engine.is_permitted( SystemPermissions.PERMIT_ADMIN_USERS, db_user): raise ParameterError() # For the admin user, also require permissions administration if user_id == 1 and not permissions_engine.is_permitted( SystemPermissions.PERMIT_ADMIN_PERMISSIONS, db_user): raise ParameterError() except ParameterError: # Roll back permissions data_engine.save_object(original_object) permissions_engine.reset() # Raise API error who = 'the \'admin\' user' if user_id == 1 else 'you' raise ParameterError( 'This change would lock %s out of administration' % who)
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 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 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 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 token(): username = None password = None # Get credentials - prefer HTTP Basic Auth if request.authorization: username = request.authorization.username password = request.authorization.password # Get credentials - but fall back to POST data if not username and not password: username = request.form.get('username', '') password = request.form.get('password', '') # Get credentials - ensure no blanks if not username: raise ParameterError('Username value cannot be blank') if not password: raise ParameterError('Password value cannot be blank') try: user = authenticate_user(username, password, data_engine, logger) except AuthenticationError as e: # Return 500 rather than 401 for authentication runtime errors raise Exception(str(e)) if user is not None: if not user.allow_api: raise SecurityError('This account is not API enabled') elif user.status != User.STATUS_ACTIVE: raise SecurityError('This account is disabled') else: # Success http_auth = TimedTokenBasicAuthentication(app) return make_api_success_response({ 'token': http_auth.generate_auth_token({'user_id': user.id}) }) # Login incorrect logger.warning('Incorrect API login for username ' + username) # Slow down scripted attacks sleep(1) raise SecurityError('Incorrect username or password')
def post(self): # Check permissions! The current user must have permissions admin to create groups. permissions_engine.ensure_permitted( SystemPermissions.PERMIT_ADMIN_PERMISSIONS, get_session_user()) params = self._get_validated_object_parameters(request.form) if params['group_type'] == Group.GROUP_TYPE_SYSTEM: raise ParameterError('System groups cannot be created') group = Group(params['name'], params['description'], params['group_type']) group.users = [] self._set_permissions(group, params) data_engine.create_group(group) return make_api_success_response(object_to_dict(group))
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 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 post(self): """ Creates a disk folder """ params = self._get_validated_parameters(request.form) try: db_folder = create_folder(params['path'], get_session_user(), data_engine, permissions_engine, logger) # Return a "fresh" object (without relationships loaded) to match PUT, DELETE db_folder = data_engine.get_folder(db_folder.id) return make_api_success_response(object_to_dict(db_folder)) except ValueError as e: if type(e) is ValueError: raise ParameterError(str(e)) else: raise # Sub-classes of ValueError
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 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 permissions permissions_engine.ensure_portfolio_permitted( folio, FolioPermission.ACCESS_EDIT, get_session_user()) # Block the export now if it would create an empty zip file if len(folio.images) == 0: raise ParameterError( 'this portfolio is empty and cannot be published') # Create a folio-export record and start the export as a background task params = self._get_validated_object_parameters(request.form) folio_export = FolioExport(folio, params['description'], params['originals'], params['image_parameters'], params['expiry_time']) data_engine.add_portfolio_history(folio, get_session_user(), FolioHistory.ACTION_PUBLISHED, folio_export.describe(True), _db_session=db_session, _commit=False) folio_export = data_engine.save_object(folio_export, refresh=True, _db_session=db_session, _commit=True) export_task = task_engine.add_task( get_session_user(), 'Export portfolio %d / export %d' % (folio.id, folio_export.id), 'export_portfolio', { 'export_id': folio_export.id, 'ignore_errors': False }, Task.PRIORITY_NORMAL, 'info', 'error', 60) # Update and return the folio-export record with the task ID folio_export.task_id = export_task.id data_engine.save_object(folio_export, _db_session=db_session, _commit=True) return make_api_success_response(object_to_dict( _prep_folioexport_object(folio, folio_export), _omit_fields + ['portfolio']), task_accepted=True) finally: db_session.close()
def put(self, image_id): """ Moves or renames a file on disk """ params = self._get_validated_parameters(request.form) # Get image data db_img = data_engine.get_image(image_id=image_id) if not db_img: raise DoesNotExistError(str(image_id)) # Move try: db_img = move_file(db_img, params['path'], get_session_user(), data_engine, permissions_engine) except ValueError as e: if type(e) is ValueError: raise ParameterError(str(e)) else: raise # Sub-classes of ValueError # Remove cached images for the 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 upload(): # Get URL parameters for the upload file_list = request.files.getlist('files') path_index = request.form.get('path_index', '-1') # Index into IMAGE_UPLOAD_DIRS or -1 path = request.form.get('path', '') # Manual path when path_index is -1 overwrite = request.form.get('overwrite') ret_dict = {} try: path_index = parse_int(path_index) overwrite = parse_boolean(overwrite) validate_string(path, 0, 1024) current_user = get_session_user() assert current_user is not None put_image_exception = None can_download = None for wkfile in file_list: original_filename = wkfile.filename if original_filename: db_image = None try: # Save (also checks user-folder permissions) _, db_image = image_engine.put_image( current_user, wkfile, secure_filename( original_filename, app.config['ALLOW_UNICODE_FILENAMES'] ), path_index, path, overwrite ) except Exception as e: # Save the error to use as our overall return value if put_image_exception is None: put_image_exception = e # This loop failure, add the error info to our return data ret_dict[original_filename] = {'error': create_api_error_dict(e)} if db_image: # Calculate download permission once (all files are going to same folder) if can_download is None: can_download = permissions_engine.is_folder_permitted( db_image.folder, FolderPermission.ACCESS_DOWNLOAD, get_session_user() ) # This loop success ret_dict[original_filename] = _image_dict(db_image, can_download) # Loop complete. If we had an exception, raise it now. if put_image_exception is not None: raise put_image_exception except Exception as e: # put_image returns ValueError for parameter errors if type(e) is ValueError: e = ParameterError(unicode(e)) # Attach whatever data we have to return with the error # Caller can then decide whether to continue if some files worked e.api_data = ret_dict raise e finally: # Store the result for the upload_complete page cache_engine.raw_put( 'UPLOAD_API:' + str(current_user.id), ret_dict, expiry_secs=(60 * 60 * 24 * 7), integrity_check=True ) # If here, all files were uploaded successfully return make_api_success_response(ret_dict)
def upload(): # Get URL parameters for the upload file_list = request.files.getlist('files') path_index = request.form.get('path_index', '-1') # Index into IMAGE_UPLOAD_DIRS or -1 path = request.form.get('path', '') # Manual path when path_index is -1 overwrite = request.form.get('overwrite') ret_dict = {} try: current_user = get_session_user() assert current_user is not None # Check params path_index = parse_int(path_index) if overwrite != 'rename': overwrite = parse_boolean(overwrite) validate_string(path, 0, 1024) if not path and path_index < 0: raise ValueError('Either path or path_index is required') if len(file_list) < 1: raise ValueError('No files have been attached') if path_index >= 0: # Get a "trusted" pre-defined upload folder # image_engine.put_image() will create it if it doesn't exist _, path = get_upload_directory(path_index) else: # A manually specified folder is "untrusted" and has to exist already if not path_exists(path): raise DoesNotExistError('Path \'' + path + '\' does not exist') # Loop over the upload files put_image_exception = None can_download = None saved_files = [] for wkfile in file_list: original_filepath = wkfile.filename original_filename = filepath_filename(original_filepath) # v2.7.1 added if original_filename: db_image = None try: # Don't allow filenames like "../../../etc/passwd" safe_filename = secure_filename( original_filename, app.config['ALLOW_UNICODE_FILENAMES'] ) # v2.7.1 If we already saved a file as safe_filename during this upload, # override this one to have overwrite=rename overwrite_flag = 'rename' if safe_filename in saved_files else overwrite # Save (this also checks user-folder permissions) _, db_image = image_engine.put_image( current_user, wkfile, path, safe_filename, overwrite_flag ) # v2.7.1 Keep a record of what filenames we used during this upload saved_files.append(safe_filename) except Exception as e: # Save the error to use as our overall return value if put_image_exception is None: put_image_exception = e # This loop failure, add the error info to our return data ret_dict[original_filepath] = { 'error': create_api_error_dict(e, logger) } if db_image: # Calculate download permission once (all files are going to same folder) if can_download is None: can_download = permissions_engine.is_folder_permitted( db_image.folder, FolderPermission.ACCESS_DOWNLOAD, get_session_user() ) # This loop success ret_dict[original_filepath] = object_to_dict( _prep_image_object(db_image, can_download), _omit_fields ) else: logger.warning('Upload received blank filename, ignoring file') # Loop complete. If we had an exception, raise it now. if put_image_exception is not None: raise put_image_exception except Exception as e: # put_image returns ValueError for parameter errors if type(e) is ValueError: e = ParameterError(str(e)) # Attach whatever data we have to return with the error # Caller can then decide whether to continue if some files worked e.api_data = ret_dict raise e finally: # Store the result for the upload_complete page cache_engine.raw_put( 'UPLOAD_API:' + str(current_user.id), ret_dict, expiry_secs=(60 * 60 * 24 * 7) ) # If here, all files were uploaded successfully return make_api_success_response(ret_dict)
def imagelist(): # Check parameters try: from_path = request.args.get('path', '') want_info = parse_boolean(request.args.get('attributes', '')) start = parse_int(request.args.get('start', '0')) limit = parse_int(request.args.get('limit', '1000')) validate_string(from_path, 1, 1024) validate_number(start, 0, 999999999) validate_number(limit, 1, 1000) except ValueError as e: raise ParameterError(e) # Get extra parameters for image URL construction, remove API parameters image_params = request.args.to_dict() image_params.pop('path', None) image_params.pop('attributes', None) image_params.pop('start', None) image_params.pop('limit', None) # Get directory listing directory_info = get_directory_listing(from_path, False, 2, start, limit) if not directory_info.exists(): raise DoesNotExistError('Invalid path') ret_list = [] db_session = data_engine.db_get_session() db_commit = False try: # Auto-populate the folders database db_folder = auto_sync_folder( from_path, data_engine, task_engine, _db_session=db_session ) db_session.commit() # Require view permission or file admin permissions_engine.ensure_folder_permitted( db_folder, FolderPermission.ACCESS_VIEW, get_session_user() ) # Get download permission in case we need to return it later can_download = permissions_engine.is_folder_permitted( db_folder, FolderPermission.ACCESS_DOWNLOAD, get_session_user() ) # Create the response file_list = directory_info.contents() supported_img_types = image_engine.get_image_formats(supported_only=True) base_folder = add_sep(directory_info.name()) for f in file_list: # v2.6.4 Return unsupported files too. If you want to reverse this change, # the filtering needs to be elsewhere for 'start' and 'limit' to work properly supported_file = get_file_extension(f['filename']) in supported_img_types file_path = base_folder + f['filename'] if want_info: # Need to return the database fields too if supported_file: db_entry = auto_sync_existing_file( file_path, data_engine, task_engine, burst_pdf=False, # Don't burst a PDF just by finding it here _db_session=db_session ) db_entry = _prep_image_object(db_entry, can_download, **image_params) else: db_entry = _prep_blank_image_object() db_entry.filename = f['filename'] db_entry.supported = False # Return images in full (standard) image dict format entry = object_to_dict(db_entry, _omit_fields) else: # Return images in short dict format entry = { 'filename': f['filename'], 'supported': supported_file, 'url': (external_url_for('image', src=file_path, **image_params) if supported_file else '') } ret_list.append(entry) db_commit = True finally: try: if db_commit: db_session.commit() else: db_session.rollback() finally: db_session.close() return make_api_success_response(ret_list)