Ejemplo n.º 1
0
    def check_transition_possible(self, current_status, new_status):
        """Check if the transition is valid."""
        valid_statuses = {
            "In Progress": "retract",
            "Available": "release",
            "Deleted": "delete",
            "Expired": "expire",
            "Archived": "archive",
        }
        if new_status not in valid_statuses:
            raise DDSArgumentError("Invalid status")

        possible_transitions = {
            "In Progress": ["Available", "Deleted", "Archived"],
            "Available": ["In Progress", "Expired", "Archived"],
            "Expired": ["Available", "Archived"],
        }

        current_transition = possible_transitions.get(current_status)
        if not current_transition:
            raise DDSArgumentError(
                message=f"Cannot change status for a project that has the status '{current_status}'."
            )

        if new_status not in current_transition:
            raise DDSArgumentError(
                message=(
                    f"You cannot {valid_statuses[new_status]} a "
                    f"project that has the current status '{current_status}'."
                )
            )
Ejemplo n.º 2
0
def check_eligibility_for_deletion(status, has_been_available):
    """Check if a project status is eligible for deletion"""
    if status not in ["In Progress"]:
        raise DDSArgumentError(
            "Project Status prevents files from being deleted.")

    if has_been_available:
        raise DDSArgumentError(
            "Existing project contents cannot be deleted since the project has been previously made available to recipients."
        )
    return True
Ejemplo n.º 3
0
    def verify_args(*args, **kwargs):

        if not flask.request.args:
            raise DDSArgumentError(
                message="Required information missing from request!")

        return func(*args, **kwargs)
Ejemplo n.º 4
0
def check_eligibility_for_download(status, user_role):
    """Check if a project status makes it eligible to download"""
    if status == "Available" or (status == "In Progress" and user_role
                                 in ["Unit Admin", "Unit Personnel"]):
        return True

    raise DDSArgumentError("Current Project status limits file download.")
Ejemplo n.º 5
0
    def delete_project(self, project: models.Project, current_time: datetime.datetime):
        """Delete project: Make status Deleted.

        Only possible from In Progress.
        """
        # Check if valid status transition
        self.check_transition_possible(current_status=project.current_status, new_status="Deleted")

        # Can only be Deleted if never made Available
        if project.has_been_available:
            raise DDSArgumentError(
                "You cannot delete a project that has been made available previously. "
                "Please abort the project if you wish to proceed."
            )
        project.is_active = False

        try:
            # Deletes files (also commits session in the function - possibly refactor later)
            RemoveContents().delete_project_contents(project=project)
            self.rm_project_user_keys(project=project)

            # Delete metadata from project row
            self.delete_project_info(proj=project)
        except (TypeError, DatabaseError, DeletionError, BucketNotFoundError) as err:
            flask.current_app.logger.exception(err)
            db.session.rollback()
            raise DeletionError(
                project=project.public_id, message="Server Error: Status was not updated"
            ) from err

        delete_message = (
            f"\nAll files in project '{project.public_id}' deleted and project info cleared"
        )

        return models.ProjectStatuses(status="Deleted", date_created=current_time), delete_message
Ejemplo n.º 6
0
    def release_project(
        self, project: models.Project, current_time: datetime.datetime, deadline_in: int
    ) -> models.ProjectStatuses:
        """Release project: Make status Available.

        Only allowed from In Progress and Expired.
        """
        # Check if valid status transition
        self.check_transition_possible(
            current_status=project.current_status, new_status="Available"
        )

        if deadline_in > 90:
            raise DDSArgumentError(
                message="The deadline needs to be less than (or equal to) 90 days."
            )

        deadline = dds_web.utils.current_time(to_midnight=True) + datetime.timedelta(
            days=deadline_in
        )

        # Project can only move from Expired 2 times
        if project.current_status == "Expired":
            if project.times_expired > 2:
                raise DDSArgumentError(
                    "Project availability limit: Project cannot be made Available any more times"
                )
        else:  # current status is in progress
            if project.has_been_available:
                # No change in deadline if made available before
                deadline = project.current_deadline
            else:
                project.released = current_time

        # Create row in ProjectStatuses
        return models.ProjectStatuses(
            status="Available", date_created=current_time, deadline=deadline
        )
Ejemplo n.º 7
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}'."
        }
Ejemplo n.º 8
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."}
Ejemplo n.º 9
0
    def archive_project(self,
                        project: models.Project,
                        current_time: datetime.datetime,
                        aborted: bool = False):
        """Archive project: Make status Archived.

        Only possible from In Progress, Available and Expired. Optional aborted flag if something
        has gone wrong.
        """
        # Check if valid status transition
        self.check_transition_possible(current_status=project.current_status,
                                       new_status="Archived")
        if project.current_status == "In Progress":
            if project.has_been_available and not aborted:
                raise DDSArgumentError(
                    "You cannot archive a project that has been made available previously. "
                    "Please abort the project if you wish to proceed.")
        project.is_active = False

        try:
            # Deletes files (also commits session in the function - possibly refactor later)
            RemoveContents().delete_project_contents(project=project)
            delete_message = f"\nAll files in {project.public_id} deleted"
            self.rm_project_user_keys(project=project)

            # Delete metadata from project row
            if aborted:
                project = self.delete_project_info(project)
                delete_message += " and project info cleared"
        except (TypeError, DatabaseError, DeletionError,
                BucketNotFoundError) as err:
            flask.current_app.logger.exception(err)
            db.session.rollback()
            raise DeletionError(
                project=project.public_id,
                message="Server Error: Status was not updated") from err

        return (
            models.ProjectStatuses(status="Archived",
                                   date_created=current_time,
                                   is_aborted=aborted),
            delete_message,
        )
Ejemplo n.º 10
0
    def expire_project(
        self, project: models.Project, current_time: datetime.datetime, deadline_in: int
    ) -> models.ProjectStatuses:
        """Expire project: Make status Expired.

        Only possible from Available.
        """
        # Check if valid status transition
        self.check_transition_possible(current_status=project.current_status, new_status="Expired")

        if deadline_in > 30:
            raise DDSArgumentError(
                message="The deadline needs to be less than (or equal to) 30 days."
            )

        deadline = dds_web.utils.current_time(to_midnight=True) + datetime.timedelta(
            days=deadline_in
        )
        return models.ProjectStatuses(
            status="Expired", date_created=current_time, deadline=deadline
        )
Ejemplo n.º 11
0
def check_eligibility_for_upload(status):
    """Check if a project status is eligible for upload/modification"""
    if status != "In Progress":
        raise DDSArgumentError(
            "Project not in right status to upload/modify files.")
    return True
Ejemplo n.º 12
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."}
Ejemplo n.º 13
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}
Ejemplo n.º 14
0
    def post(self):
        """Create a new project."""
        p_info = flask.request.json

        # Verify enough number of Unit Admins or return message
        force_create = p_info.pop("force", False)
        if not isinstance(force_create, bool):
            raise DDSArgumentError(message="`force` is a boolean value: True or False.")

        warning_message = dds_web.utils.verify_enough_unit_admins(
            unit_id=auth.current_user().unit.id, force_create=force_create
        )
        if warning_message:
            return {"warning": warning_message}

        # Add a new project to db
        import pymysql

        try:
            new_project = project_schemas.CreateProjectSchema().load(p_info)
            db.session.add(new_project)
        except sqlalchemy.exc.OperationalError as err:
            raise DatabaseError(message=str(err), alt_message="Unexpected database error.")

        if not new_project:
            raise DDSArgumentError("Failed to create project.")

        # TODO: Change -- the bucket should be created before the row is added to the database
        # This is a quick fix so that things do not break
        with ApiS3Connector(project=new_project) as s3:
            try:
                s3.resource.create_bucket(Bucket=new_project.bucket)
            except (
                botocore.exceptions.ClientError,
                botocore.exceptions.ParamValidationError,
            ) as err:
                # For now just keeping the project row
                raise S3ConnectionError(str(err)) from err

        try:
            db.session.commit()
        except (sqlalchemy.exc.SQLAlchemyError, sqlalchemy.exc.OperationalError, TypeError) as err:
            flask.current_app.logger.exception(err)
            db.session.rollback()
            raise DatabaseError(
                message=str(err),
                alt_message=(
                    "Project was not created"
                    + (
                        ": Database malfunction."
                        if isinstance(err, sqlalchemy.exc.OperationalError)
                        else ": Server error."
                    ),
                ),
            ) from err
        except (
            marshmallow.exceptions.ValidationError,
            DDSArgumentError,
            AccessDeniedError,
        ) as err:
            flask.current_app.logger.exception(err)
            db.session.rollback()
            raise

        flask.current_app.logger.debug(
            f"Project {new_project.public_id} created by user {auth.current_user().username}."
        )

        user_addition_statuses = []
        if "users_to_add" in p_info:
            for user in p_info["users_to_add"]:
                try:
                    existing_user = user_schemas.UserSchema().load(user)
                    unanswered_invite = user_schemas.UnansweredInvite().load(user)
                except (
                    marshmallow.exceptions.ValidationError,
                    sqlalchemy.exc.OperationalError,
                ) as err:
                    if isinstance(err, sqlalchemy.exc.OperationalError):
                        flask.current_app.logger.error(err)
                        addition_status = "Unexpected database error."
                    else:
                        addition_status = f"Error for {user.get('email')}: {err}"
                    user_addition_statuses.append(addition_status)
                    continue

                if not existing_user and not unanswered_invite:
                    # Send invite if the user doesn't exist
                    invite_user_result = AddUser.invite_user(
                        email=user.get("email"),
                        new_user_role=user.get("role"),
                        project=new_project,
                    )

                    if invite_user_result["status"] == http.HTTPStatus.OK:
                        invite_msg = (
                            f"Invitation sent to {user['email']}. "
                            "The user should have a valid account to be added to a project"
                        )
                    else:
                        invite_msg = invite_user_result["message"]
                    user_addition_statuses.append(invite_msg)
                else:
                    # If it is an existing user, add them to project.
                    addition_status = ""
                    try:
                        add_user_result = AddUser.add_to_project(
                            whom=existing_user or unanswered_invite,
                            project=new_project,
                            role=user.get("role"),
                        )
                    except DatabaseError as err:
                        addition_status = f"Error for {user['email']}: {err.description}"
                    else:
                        addition_status = add_user_result["message"]
                    user_addition_statuses.append(addition_status)

        return {
            "status": http.HTTPStatus.OK,
            "message": f"Added new project '{new_project.title}'",
            "project_id": new_project.public_id,
            "user_addition_statuses": user_addition_statuses,
        }