def post(self): """Associate existing users or unanswered invites with projects or create invites""" args = flask.request.args json_info = flask.request.json # Verify valid role (should also catch None) role = json_info.get("role") if not dds_web.utils.valid_user_role(specified_role=role): raise ddserr.DDSArgumentError(message="Invalid user role.") # Unit only changable for Super Admin invites unit = json_info.get( "unit") if auth.current_user().role == "Super Admin" else None # A project may or may not be specified project = args.get("project") if args else None if project: project = project_schemas.ProjectRequiredSchema().load( {"project": project}) # Verify email email = json_info.get("email") if not email: raise ddserr.DDSArgumentError( message="Email address required to add or invite.") # Notify the users about project additions? Invites are still being sent out. send_email = json_info.get("send_email", True) # Check if email is registered to a user try: existing_user = user_schemas.UserSchema().load({"email": email}) unanswered_invite = user_schemas.UnansweredInvite().load( {"email": email}) except sqlalchemy.exc.OperationalError as err: raise ddserr.DatabaseError( message=str(err), alt_message="Unexpected database error.") if existing_user or unanswered_invite: if not project: raise ddserr.DDSArgumentError(message=( "This user was already added to the system. " "Specify the project you wish to give access to.")) add_user_result = self.add_to_project( whom=existing_user or unanswered_invite, project=project, role=role, send_email=send_email, ) return add_user_result, add_user_result["status"] else: # Send invite if the user doesn't exist invite_user_result = self.invite_user(email=email, new_user_role=role, project=project, unit=unit) return invite_user_result, invite_user_result["status"]
def get(self): """Get the safespring project.""" # Verify project ID and access project = project_schemas.ProjectRequiredSchema().load(flask.request.args) check_eligibility_for_upload(status=project.current_status) try: sfsp_proj, keys, url, bucketname = ApiS3Connector(project=project).get_s3_info() except (sqlalchemy.exc.SQLAlchemyError, sqlalchemy.exc.OperationalError) as sqlerr: raise DatabaseError( message=str(sqlerr), alt_message="Could not get cloud information" + ( ": Database malfunction." if isinstance(sqlerr, sqlalchemy.exc.OperationalError) else "." ), ) from sqlerr if any(x is None for x in [url, keys, bucketname]): raise S3ProjectNotFoundError("No s3 info returned!") return { "safespring_project": sfsp_proj, "url": url, "keys": keys, "bucket": bucketname, }
def get(self): # Verify project ID and access project = project_schemas.ProjectRequiredSchema().load(flask.request.args) # Get info on research users research_users = list() for user in project.researchusers: user_info = { "User Name": user.user_id, "Primary email": "", "Role": "Owner" if user.owner else "Researcher", } for user_email in user.researchuser.emails: if user_email.primary: user_info["Primary email"] = user_email.email research_users.append(user_info) for invitee in project.project_invite_keys: role = "Owner" if invitee.owner else "Researcher" user_info = { "User Name": "NA (Pending)", "Primary email": f"{invitee.invite.email} (Pending)", "Role": f"{role} (Pending)", } research_users.append(user_info) return {"research_users": research_users}
def get(self): """Get a list of files within the specified folder.""" # Verify project ID and access project = project_schemas.ProjectRequiredSchema().load( flask.request.args) if auth.current_user( ).role == "Researcher" and project.current_status == "In Progress": raise AccessDeniedError( message="There's no data available at this time.") extra_args = flask.request.json if extra_args is None: extra_args = {} # Check if to return file size show_size = extra_args.get("show_size") # Check if to get from root or folder subpath = "." if extra_args.get("subpath"): subpath = extra_args.get("subpath").rstrip(os.sep) files_folders = list() # Check project not empty if project.num_files == 0: return { "num_items": 0, "message": f"The project {project.public_id} is empty." } # Get files and folders distinct_files, distinct_folders = self.items_in_subpath( project=project, folder=subpath) # Collect file and folder info to return to CLI if distinct_files: for x in distinct_files: info = { "name": x[0] if subpath == "." else x[0].split(os.sep)[-1], "folder": False, } if show_size: info.update({"size": x[1]}) files_folders.append(info) if distinct_folders: for x in distinct_folders: info = { "name": x if subpath == "." else x.split(os.sep)[-1], "folder": True, } if show_size: folder_size = self.get_folder_size(project=project, folder_name=x) info.update({"size": folder_size}) files_folders.append(info) return {"files_folders": files_folders}
def get(self): """Checks which files can be downloaded, and get their info.""" # Verify project ID and access project = project_schemas.ProjectRequiredSchema().load( flask.request.args) # Verify project status ok for download user_role = auth.current_user().role check_eligibility_for_download(status=project.current_status, user_role=user_role) # Get project contents input_ = { "project": project.public_id, **{ "requested_items": flask.request.json, "url": True }, } ( found_files, found_folder_contents, not_found, ) = project_schemas.ProjectContentSchema().dump(input_) return { "files": found_files, "folder_contents": found_folder_contents, "not_found": not_found, }
def post(self): """Add new file to DB.""" # Verify project id and access project = project_schemas.ProjectRequiredSchema().load( flask.request.args) # Verify that project has correct status for upload check_eligibility_for_upload(status=project.current_status) # Create new files new_file = file_schemas.NewFileSchema().load({ **flask.request.json, "project": project.public_id }) try: db.session.commit() except (sqlalchemy.exc.SQLAlchemyError, sqlalchemy.exc.OperationalError) as err: flask.current_app.logger.debug(err) db.session.rollback() raise DatabaseError( message=str(err), alt_message="Failed to add new file to database" + (": Database malfunction." if isinstance( err, sqlalchemy.exc.OperationalError) else "."), ) from err return {"message": f"File '{new_file.name}' added to db."}
def post(self): """Remove a user from a project""" project = project_schemas.ProjectRequiredSchema().load(flask.request.args) json_input = flask.request.json if not (user_email := json_input.get("email")): raise ddserr.DDSArgumentError(message="User email missing.")
def get(self): """Get name in bucket for all files specified.""" # Verify project ID and access project = project_schemas.ProjectRequiredSchema().load( flask.request.args) # Verify project has correct status for upload check_eligibility_for_upload(status=project.current_status) # Get files specified try: matching_files = (models.File.query.filter( models.File.name.in_(flask.request.json)).filter( models.File.project_id == sqlalchemy.func.binary( project.id)).all()) except (sqlalchemy.exc.SQLAlchemyError, sqlalchemy.exc.OperationalError) as err: raise DatabaseError( message=str(err), alt_message=f"Failed to get matching files in db" + (": Database malfunction." if isinstance( err, sqlalchemy.exc.OperationalError) else "."), ) from err # The files checked are not in the db if not matching_files or matching_files is None: return {"files": None} return {"files": {x.name: x.name_in_bucket for x in matching_files}}
def get(self): """Get public key from database.""" # Verify project ID and access project = project_schemas.ProjectRequiredSchema().load(flask.request.args) flask.current_app.logger.debug("Getting the public key.") if not project.public_key: raise KeyNotFoundError(project=project.public_id) return {"public": project.public_key.hex().upper()}
def get(self): """Get private key from database.""" # Verify project ID and access project = project_schemas.ProjectRequiredSchema().load( flask.request.args) flask.current_app.logger.debug("Getting the private key.") return flask.jsonify({ "private": obtain_project_private_key( user=auth.current_user(), project=project, token=dds_web.security.auth.obtain_current_encrypted_token(), ).hex().upper() })
def delete(self): """Delete file(s).""" # Verify project ID and access project = project_schemas.ProjectRequiredSchema().load( flask.request.args) # Verify project status ok for deletion check_eligibility_for_deletion( status=project.current_status, has_been_available=project.has_been_available) # Delete file(s) from db and cloud not_removed_dict, not_exist_list = self.delete_multiple( project=project, files=flask.request.json) # Return deleted and not deleted files return {"not_removed": not_removed_dict, "not_exists": not_exist_list}
def post(self): """Give access to user.""" # Verify that user specified json_input = flask.request.json if "email" not in json_input: raise DDSArgumentError(message="User email missing.") user = user_schemas.UserSchema().load( {"email": json_input.pop("email")}) if not user: raise NoSuchUserError() # Verify that project specified project_info = flask.request.args project = None if project_info and project_info.get("project"): project = project_schemas.ProjectRequiredSchema().load( project_info) # Verify permission to give user access self.verify_renew_access_permission(user=user, project=project) # Give access to specific project or all active projects if no project specified list_of_projects = None if not project: if user.role == "Researcher": list_of_projects = [ x.project for x in user.project_associations ] elif user.role in ["Unit Personnel", "Unit Admin"]: list_of_projects = user.unit.projects else: list_of_projects = [project] errors = self.give_project_access(project_list=list_of_projects, current_user=auth.current_user(), user=user) if errors: return {"errors": errors} return { "message": f"Project access updated for user '{user.primary_email}'." }
def put(self): """Update info in db.""" # Verify project ID and access project = project_schemas.ProjectRequiredSchema().load( flask.request.args) # Get file name from request from CLI file_name = flask.request.json.get("name") if not file_name: raise DDSArgumentError( "No file name specified. Cannot update file.") # Update file info try: flask.current_app.logger.debug( "Updating file in current project: %s", project.public_id) flask.current_app.logger.debug(f"File name: {file_name}") file = models.File.query.filter( sqlalchemy.and_( models.File.project_id == sqlalchemy.func.binary( project.id), models.File.name == sqlalchemy.func.binary(file_name), )).first() if not file: raise NoSuchFileError() file.time_latest_download = dds_web.utils.current_time() except (sqlalchemy.exc.SQLAlchemyError, sqlalchemy.exc.OperationalError) as err: db.session.rollback() flask.current_app.logger.exception(str(err)) raise DatabaseError( message=str(err), alt_message="Update of file info failed" + (": Database malfunction." if isinstance( err, sqlalchemy.exc.OperationalError) else "."), ) from err else: # flask.current_app.logger.debug("File %s updated", file_name) db.session.commit() return {"message": "File info updated."}
def delete(self): """Removes all project contents.""" # Verify project ID and access project = project_schemas.ProjectRequiredSchema().load(flask.request.args) # Verify project status ok for deletion check_eligibility_for_deletion( status=project.current_status, has_been_available=project.has_been_available ) # Check if project contains anything if not project.files: raise EmptyProjectException( project=project, message="There are no project contents to delete." ) # Delete project contents from db and cloud self.delete_project_contents(project=project) return {"removed": True}
def get(self): """Get current project status and optionally entire status history""" # Verify project ID and access project = project_schemas.ProjectRequiredSchema().load(flask.request.args) # Get current status and deadline return_info = {"current_status": project.current_status} if project.current_deadline: return_info["current_deadline"] = project.current_deadline # Get status history json_input = flask.request.json if json_input and json_input.get("history"): history = [] for pstatus in project.project_statuses: history.append(tuple((pstatus.status, pstatus.date_created))) history.sort(key=lambda x: x[1], reverse=True) return_info.update({"history": history}) return return_info
def get(self): """Get file info on all files.""" # Verify project ID and access project = project_schemas.ProjectRequiredSchema().load( flask.request.args) # Verify project status ok for download user_role = auth.current_user().role check_eligibility_for_download(status=project.current_status, user_role=user_role) files, _, _ = project_schemas.ProjectContentSchema().dump({ "project": project.public_id, "get_all": True, "url": True }) return {"files": files}
def put(self): """Update existing file.""" # Verify project ID and access project = project_schemas.ProjectRequiredSchema().load( flask.request.args) # Verify that projet has correct status for upload check_eligibility_for_upload(status=project.current_status) file_info = flask.request.json if not all(x in file_info for x in ["name", "name_in_bucket", "subpath", "size"]): raise DDSArgumentError( "Information is missing, cannot add file to database.") try: # Check if file already in db existing_file = models.File.query.filter( sqlalchemy.and_( models.File.name == sqlalchemy.func.binary( file_info.get("name")), models.File.project_id == project.id, )).first() # Error if not found if not existing_file or existing_file is None: raise NoSuchFileError( "Cannot update non-existent file " f"'{werkzeug.utils.secure_filename(file_info.get('name'))}' in the database!" ) # Get version row current_file_version = models.Version.query.filter( sqlalchemy.and_( models.Version.active_file == sqlalchemy.func.binary( existing_file.id), models.Version.time_deleted.is_(None), )).all() if len(current_file_version) > 1: flask.current_app.logger.warning( "There is more than one version of the file " "which does not yet have a deletion timestamp.") # Same timestamp for deleted and created new file new_timestamp = dds_web.utils.current_time() # Overwritten == deleted/deactivated for version in current_file_version: if version.time_deleted is None: version.time_deleted = new_timestamp # Update file info existing_file.subpath = file_info.get("subpath") existing_file.size_original = file_info.get("size") existing_file.size_stored = file_info.get("size_processed") existing_file.compressed = file_info.get("compressed") existing_file.salt = file_info.get("salt") existing_file.public_key = file_info.get("public_key") existing_file.time_uploaded = new_timestamp existing_file.checksum = file_info.get("checksum") # New version new_version = models.Version( size_stored=file_info.get("size_processed"), time_uploaded=new_timestamp, active_file=existing_file.id, project_id=project, ) # Update foreign keys and relationships project.file_versions.append(new_version) existing_file.versions.append(new_version) db.session.add(new_version) db.session.commit() except (sqlalchemy.exc.SQLAlchemyError, sqlalchemy.exc.OperationalError) as err: db.session.rollback() raise DatabaseError( message=str(err), alt_message=f"Failed updating file information" + (": Database malfunction." if isinstance( err, sqlalchemy.exc.OperationalError) else "."), ) from err return {"message": f"File '{file_info.get('name')}' updated in db."}
def delete(self): """Delete folder(s).""" # Verify project ID and access project = project_schemas.ProjectRequiredSchema().load( flask.request.args) # Verify project status ok for deletion check_eligibility_for_deletion( status=project.current_status, has_been_available=project.has_been_available) # Remove folder(s) not_removed, not_exist = ({}, []) fail_type = None with ApiS3Connector(project=project) as s3conn: for folder_name in flask.request.json: # Get all files in the folder files = self.get_files_for_deletion(project=project, folder=folder_name) if not files: not_exist.append(folder_name) continue # S3 can only delete 1000 files per request # The deletion will thus be divided into batches of at most 1000 files batch_size: int = 1000 for i in range(0, len(files), batch_size): # Delete from s3 bucket_names = tuple(entry.name_in_bucket for entry in files[i:i + batch_size]) try: s3conn.remove_multiple(items=bucket_names, batch_size=batch_size) except botocore.client.ClientError as err: not_removed[folder_name] = str(err) fail_type = "s3" break # Commit to db if no error so far try: self.queue_file_entry_deletion(files[i:i + batch_size]) project.date_updated = dds_web.utils.current_time() db.session.commit() except (sqlalchemy.exc.SQLAlchemyError, sqlalchemy.exc.OperationalError) as err: db.session.rollback() flask.current_app.logger.error( "Files deleted in S3 but not in db. The entries must be synchronised! " f"Error: {str(err)}") not_removed[ folder_name] = "Could not remove files in folder" + ( ": Database malfunction." if isinstance( err, sqlalchemy.exc.OperationalError) else ".") fail_type = "db" break return { "not_removed": not_removed, "fail_type": fail_type, "not_exists": not_exist, "nr_deleted": len(files) if not not_removed else i, }
def post(self): """Update Project Status.""" # Verify project ID and access project = project_schemas.ProjectRequiredSchema().load(flask.request.args) # Check if valid status json_input = flask.request.json new_status = json_input.get("new_status") if not new_status: raise DDSArgumentError(message="No status transition provided. Specify the new status.") # Override default to send email send_email = json_input.get("send_email", True) # Initial variable definition curr_date = dds_web.utils.current_time() delete_message = "" is_aborted = False # Moving to Available if new_status == "Available": deadline_in = json_input.get("deadline", project.responsible_unit.days_in_available) new_status_row = self.release_project( project=project, current_time=curr_date, deadline_in=deadline_in ) elif new_status == "In Progress": new_status_row = self.retract_project(project=project, current_time=curr_date) elif new_status == "Expired": deadline_in = json_input.get("deadline", project.responsible_unit.days_in_expired) new_status_row = self.expire_project( project=project, current_time=curr_date, deadline_in=deadline_in ) elif new_status == "Deleted": new_status_row, delete_message = self.delete_project( project=project, current_time=curr_date ) elif new_status == "Archived": is_aborted = json_input.get("is_aborted", False) new_status_row, delete_message = self.archive_project( project=project, current_time=curr_date, aborted=is_aborted ) else: raise DDSArgumentError(message="Invalid status") try: project.project_statuses.append(new_status_row) db.session.commit() except (sqlalchemy.exc.OperationalError, sqlalchemy.exc.SQLAlchemyError) as err: flask.current_app.logger.exception(err) db.session.rollback() raise DatabaseError( message=str(err), alt_message=( "Status was not updated" + ( ": Database malfunction." if isinstance(err, sqlalchemy.exc.OperationalError) else ": Server Error." ) ), ) from err # Mail users once project is made available if new_status == "Available" and send_email: for user in project.researchusers: AddUser.compose_and_send_email_to_user( userobj=user.researchuser, mail_type="project_release", project=project ) return_message = f"{project.public_id} updated to status {new_status}" + ( " (aborted)" if new_status == "Archived" and is_aborted else "" ) if new_status != "Available": return_message += delete_message + "." else: return_message += ( f". An e-mail notification has{' not ' if not send_email else ' '}been sent." ) return {"message": return_message}