Пример #1
0
    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"]
Пример #2
0
    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,
        }
Пример #3
0
    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}
Пример #4
0
    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}
Пример #5
0
    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,
        }
Пример #6
0
    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."}
Пример #7
0
    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.")
Пример #8
0
    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}}
Пример #9
0
    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()}
Пример #10
0
    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()
        })
Пример #11
0
    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}
Пример #12
0
    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}'."
        }
Пример #13
0
    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."}
Пример #14
0
    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}
Пример #15
0
    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
Пример #16
0
    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}
Пример #17
0
    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."}
Пример #18
0
    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,
        }
Пример #19
0
    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}