Beispiel #1
0
    def create_project(self, data, **kwargs):
        """Create project row in db."""

        # Lock db, get unit row and update counter
        unit_row = (models.Unit.query.filter_by(
            id=auth.current_user().unit_id).with_for_update().one_or_none())
        if not unit_row:
            raise ddserr.AccessDeniedError(
                message="Error: Your user is not associated to a unit.")

        unit_row.counter = unit_row.counter + 1 if unit_row.counter else 1
        data["public_id"] = "{}{:05d}".format(unit_row.internal_ref,
                                              unit_row.counter)

        # Generate bucket name
        data["bucket"] = self.generate_bucketname(
            public_id=data["public_id"], created_time=data["date_created"])

        # Create project
        current_user = auth.current_user()
        new_project = models.Project(
            **{
                **data, "unit_id": current_user.unit.id,
                "created_by": current_user.username
            })
        new_project.project_statuses.append(
            models.ProjectStatuses(**{
                "status": "In Progress",
                "date_created": data["date_created"],
            }))
        generate_project_key_pair(current_user, new_project)

        return new_project
Beispiel #2
0
    def verify_renew_access_permission(user, project):
        """Check that user has permission to give access to another user in this project."""

        if auth.current_user() == user:
            raise AccessDeniedError(message="You cannot renew your own access.")

        # Get roles
        current_user_role = get_user_roles_common(user=auth.current_user())
        other_user_role = get_user_roles_common(user=user)

        # Check access
        if not (
            (
                current_user_role == "Unit Admin"
                and other_user_role
                in ["Unit Admin", "Unit Personnel", "Project Owner", "Researcher"]
            )
            or (
                current_user_role == "Unit Personnel"
                and other_user_role in ["Unit Personnel", "Project Owner", "Researcher"]
            )
            or (
                current_user_role == "Project Owner"
                and other_user_role in ["Project Owner", "Researcher"]
            )
        ):
            raise AccessDeniedError(
                message=(
                    "You do not have the necessary permissions "
                    "to shared project access with this user."
                )
            )
Beispiel #3
0
    def get(self):
        """List unit users within the unit the current user is connected to, or the one defined by a superadmin."""
        unit_users = {}

        if auth.current_user().role == "Super Admin":
            json_input = flask.request.json
            if not json_input:
                raise ddserr.DDSArgumentError(message="Unit public id missing.")

            unit = json_input.get("unit")
            if not unit:
                raise ddserr.DDSArgumentError(message="Unit public id missing.")

            unit_row = models.Unit.query.filter_by(public_id=unit).one_or_none()
            if not unit_row:
                raise ddserr.DDSArgumentError(
                    message=f"There is no unit with the public id '{unit}'."
                )
        else:
            unit_row = auth.current_user().unit

        keys = ["Name", "Username", "Email", "Role", "Active"]

        unit_users = [
            {
                "Name": user.name,
                "Username": user.username,
                "Email": user.primary_email,
                "Role": user.role,
                "Active": user.is_active,
            }
            for user in unit_row.users
        ]

        return {"users": unit_users, "keys": keys, "unit": unit_row.name}
Beispiel #4
0
def verify_project_access(project):
    """Check users access to project."""

    if project not in auth.current_user().projects:
        raise ddserr.AccessDeniedError(
            message="Project access denied.",
            username=auth.current_user().username,
            project=project.public_id,
        )

    return project
Beispiel #5
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}
Beispiel #6
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"]
Beispiel #7
0
    def delete_invite(email):
        current_user_role = auth.current_user().role
        try:
            unanswered_invite = user_schemas.UnansweredInvite().load(
                {"email": email})
            if unanswered_invite:
                if current_user_role == "Super Admin" or (
                        current_user_role == "Unit Admin"
                        and unanswered_invite.role
                        in ["Unit Admin", "Unit Personnel", "Researcher"]):
                    db.session.delete(unanswered_invite)
                    db.session.commit()
                else:
                    raise ddserr.AccessDeniedError(
                        message=
                        "You do not have the correct permissions to delete this invite."
                    )
        except (sqlalchemy.exc.SQLAlchemyError,
                sqlalchemy.exc.OperationalError) as err:
            db.session.rollback()
            flask.current_app.logger.error(
                "The invite connected to the email "
                f"{email or '[no email provided]'} was not deleted.")
            raise ddserr.DatabaseError(
                message=str(err),
                alt_message=f"Failed to delete invite" +
                (": Database malfunction." if isinstance(
                    err, sqlalchemy.exc.OperationalError) else "."),
            ) from err

        return email
Beispiel #8
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,
        }
Beispiel #9
0
 def get(self):
     return {
         "message": "Please take this token to /user/second_factor to authenticate with MFA!",
         "token": encrypted_jwt_token(
             username=auth.current_user().username,
             sensitive_content=flask.request.authorization.get("password"),
         ),
     }
Beispiel #10
0
def get_username_or_request_ip():
    """Util function for action logger: Try to identify the requester"""

    if auth.current_user():
        current_user = auth.current_user().username
    elif flask_login.current_user.is_authenticated:
        current_user = flask_login.current_user.username
    else:
        username = (
            flask.request.authorization.get("username") if flask.request.authorization else "---"
        )
        if flask.request.remote_addr:
            current_user = f"{username} ({flask.request.remote_addr})"  # log IP instead of username
        elif flask.request.access_route:
            current_user = (
                f"{username} ({flask.request.access_route[0]})"  # log IP instead of username
            )
        else:
            current_user = f"{username} (anonymous)"

    return current_user
Beispiel #11
0
 def get(self):
     """Return own info when queried"""
     curr_user = auth.current_user()
     info = {}
     info["email_primary"] = curr_user.primary_email
     info["emails_all"] = [x.email for x in curr_user.emails]
     info["role"] = curr_user.role
     info["username"] = curr_user.username
     info["name"] = curr_user.name
     if "Unit" in curr_user.role and curr_user.is_admin:
         info["is_admin"] = curr_user.is_admin
     return {"info": info}
Beispiel #12
0
    def __init__(self,
                 project,
                 username=None,
                 message="The project is empty."):

        if not username:
            username = auth.current_user()
        structlog.threadlocal.bind_threadlocal(user=username)
        if project:
            structlog.threadlocal.bind_threadlocal(project=project)

        general_logger.warning(message)
        super().__init__(message)
Beispiel #13
0
    def validate_mfa(self, data, **kwargs):
        """Verify HOTP (authentication One-Time code) is correct."""

        # This can be easily extended to require at least one MFA method
        if "HOTP" not in data:
            raise marshmallow.exceptions.ValidationError(
                "MFA method not supplied")

        user = auth.current_user()
        if "HOTP" in data:
            value = data.get("HOTP")
            # Raises authenticationerror if invalid
            user.verify_HOTP(value.encode())
Beispiel #14
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()
        })
Beispiel #15
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}'."
        }
Beispiel #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}
Beispiel #17
0
    def invite_user(email, new_user_role, project=None, unit=None):
        """Invite a new user"""

        current_user_role = get_user_roles_common(user=auth.current_user())

        if not project:
            if current_user_role == "Project Owner":
                return {
                    "status": ddserr.InviteError.code.value,
                    "message":
                    "Project ID required to invite users to projects.",
                }
            if new_user_role == "Project Owner":
                return {
                    "status": ddserr.InviteError.code.value,
                    "message":
                    "Project ID required to invite a 'Project Owner'.",
                }

        # Verify role or current and new user
        if current_user_role == "Super Admin" and project:
            return {
                "status":
                ddserr.InviteError.code.value,
                "message":
                ("Super Admins do not have project data access and can therefore "
                 "not invite users to specific projects."),
            }
        elif current_user_role == "Unit Admin" and new_user_role == "Super Admin":
            return {
                "status": ddserr.AccessDeniedError.code.value,
                "message": ddserr.AccessDeniedError.description,
            }
        elif current_user_role == "Unit Personnel" and new_user_role in [
                "Super Admin",
                "Unit Admin",
        ]:
            return {
                "status": ddserr.AccessDeniedError.code.value,
                "message": ddserr.AccessDeniedError.description,
            }
        elif current_user_role == "Project Owner" and new_user_role in [
                "Super Admin",
                "Unit Admin",
                "Unit Personnel",
        ]:
            return {
                "status": ddserr.AccessDeniedError.code.value,
                "message": ddserr.AccessDeniedError.description,
            }
        elif current_user_role == "Researcher":
            return {
                "status": ddserr.AccessDeniedError.code.value,
                "message": ddserr.AccessDeniedError.description,
            }

        # Create invite row
        new_invite = models.Invite(
            email=email,
            role=("Researcher"
                  if new_user_role == "Project Owner" else new_user_role),
        )

        # Create URL safe token for invitation link
        token = encrypted_jwt_token(
            username="",
            sensitive_content=generate_invite_key_pair(
                invite=new_invite).hex(),
            expires_in=datetime.timedelta(
                hours=flask.current_app.config["INVITATION_EXPIRES_IN_HOURS"]),
            additional_claims={"inv": new_invite.email},
        )

        # Create link for invitation email
        link = flask.url_for("auth_blueprint.confirm_invite",
                             token=token,
                             _external=True)

        # Quick search gave this as the URL length limit.
        if len(link) >= 2048:
            flask.current_app.logger.error(
                "Invitation link was not possible to create due to length.")
            return {
                "message": "Invite failed due to server error",
                "status": http.HTTPStatus.INTERNAL_SERVER_ERROR,
            }

        projects_not_shared = {}
        goahead = False
        # Append invite to unit if applicable
        if new_invite.role in ["Unit Admin", "Unit Personnel"]:
            # TODO Change / move this later. This is just so that we can add an initial Unit Admin.
            if auth.current_user().role == "Super Admin":
                if unit:
                    unit_row = models.Unit.query.filter_by(
                        public_id=unit).one_or_none()
                    if not unit_row:
                        raise ddserr.DDSArgumentError(
                            message="Invalid unit publid id.")

                    unit_row.invites.append(new_invite)
                    goahead = True
                else:
                    raise ddserr.DDSArgumentError(
                        message=
                        "You need to specify a unit to invite a Unit Personnel or Unit Admin."
                    )

            if "Unit" in auth.current_user().role:
                # Give new unit user access to all projects of the unit
                auth.current_user().unit.invites.append(new_invite)
                if auth.current_user().unit.projects:
                    for unit_project in auth.current_user().unit.projects:
                        if unit_project.is_active:
                            try:
                                share_project_private_key(
                                    from_user=auth.current_user(),
                                    to_another=new_invite,
                                    from_user_token=dds_web.security.auth.
                                    obtain_current_encrypted_token(),
                                    project=unit_project,
                                )
                            except ddserr.KeyNotFoundError as keyerr:
                                projects_not_shared[
                                    unit_project.
                                    public_id] = "You do not have access to the project(s)"
                            else:
                                goahead = True
                else:
                    goahead = True

                if not project:  # specified project is disregarded for unituser invites
                    msg = f"{str(new_invite)} was successful."
                else:
                    msg = f"{str(new_invite)} was successful, but specification for {str(project)} dropped. Unit Users have automatic access to projects of their unit."

        else:
            db.session.add(new_invite)
            if project:
                try:
                    share_project_private_key(
                        from_user=auth.current_user(),
                        to_another=new_invite,
                        project=project,
                        from_user_token=dds_web.security.auth.
                        obtain_current_encrypted_token(),
                        is_project_owner=new_user_role == "Project Owner",
                    )
                except ddserr.KeyNotFoundError as keyerr:
                    projects_not_shared[
                        project.
                        public_id] = "You do not have access to the specified project."
                else:
                    goahead = True
            else:
                goahead = True

        # Compose and send email
        status_code = http.HTTPStatus.OK
        if goahead:
            try:
                db.session.commit()
            except (sqlalchemy.exc.SQLAlchemyError,
                    sqlalchemy.exc.OperationalError) as sqlerr:
                db.session.rollback()
                raise ddserr.DatabaseError(
                    message=str(sqlerr),
                    alt_message=f"Invitation failed" +
                    (": Database malfunction." if isinstance(
                        sqlerr, sqlalchemy.exc.OperationalError) else "."),
                ) from sqlerr

            AddUser.compose_and_send_email_to_user(userobj=new_invite,
                                                   mail_type="invite",
                                                   link=link)
            msg = f"{str(new_invite)} was successful."
        else:
            msg = (f"The user could not be added to the project(s)."
                   if projects_not_shared else
                   "Unknown error!") + " The invite did not succeed."
            status_code = ddserr.InviteError.code.value

        return {
            "email": new_invite.email,
            "message": msg,
            "status": status_code,
            "errors": projects_not_shared,
        }
Beispiel #18
0
    def get(self):
        current_user = auth.current_user()

        # Check that user is unit account
        if current_user.role not in ["Unit Admin", "Unit Personnel"]:
            raise ddserr.AccessDeniedError(
                "Access denied - only unit accounts can get invoicing information."
            )

        # Get unit info from table (incl safespring proj name)
        try:
            unit_info = models.Unit.query.filter(
                models.Unit.id == sqlalchemy.func.binary(
                    current_user.unit_id)).first()
        except (sqlalchemy.exc.SQLAlchemyError,
                sqlalchemy.exc.OperationalError) as err:
            flask.current_app.logger.exception(err)
            raise ddserr.DatabaseError(
                message=str(err),
                alt_message=f"Failed to get unit information." +
                (": Database malfunction." if isinstance(
                    err, sqlalchemy.exc.OperationalError) else "."),
            ) from err

        # Total number of GB hours and cost saved in the db for the specific unit
        total_gbhours_db = 0.0
        total_cost_db = 0.0

        # Project (bucket) specific info
        usage = {}
        for p in unit_info.projects:

            # Define fields in usage dict
            usage[p.public_id] = {"gbhours": 0.0, "cost": 0.0}

            for f in p.files:
                for v in f.versions:
                    # Calculate hours of the current file
                    time_uploaded = v.time_uploaded
                    time_deleted = (v.time_deleted if v.time_deleted else
                                    dds_web.utils.current_time())
                    file_hours = (time_deleted - time_uploaded).seconds / (60 *
                                                                           60)

                    # Calculate GBHours, if statement to avoid zerodivision exception
                    gb_hours = ((v.size_stored / 1e9) /
                                file_hours) if file_hours else 0.0

                    # Save file version gbhours to project info and increase total unit sum
                    usage[p.public_id]["gbhours"] += gb_hours
                    total_gbhours_db += gb_hours

                    # Calculate approximate cost per gbhour: kr per gb per month / (days * hours)
                    cost_gbhour = 0.09 / (30 * 24)
                    cost = gb_hours * cost_gbhour

                    # Save file cost to project info and increase total unit cost
                    usage[p.public_id]["cost"] += cost
                    total_cost_db += cost

            usage[p.public_id].update({
                "gbhours":
                round(usage[p.public_id]["gbhours"], 2),
                "cost":
                round(usage[p.public_id]["cost"], 2),
            })

        return {
            "total_usage": {
                "gbhours": round(total_gbhours_db, 2),
                "cost": round(total_cost_db, 2),
            },
            "project_usage": usage,
        }
Beispiel #19
0
    def delete(self):
        """Delete user or invite in the DDS."""
        current_user = auth.current_user()

        json_info = flask.request.json
        if json_info:
            is_invite = json_info.pop("is_invite", False)
            if is_invite:
                email = self.delete_invite(email=json_info.get("email"))
                return {
                    "message": ("The invite connected to email "
                                f"'{email}' has been deleted.")
                }

        try:
            user = user_schemas.UserSchema().load(json_info)
        except sqlalchemy.exc.OperationalError as err:
            raise ddserr.DatabaseError(
                message=str(err), alt_message="Unexpected database error.")

        if not user:
            raise ddserr.UserDeletionError(message=(
                "This e-mail address is not associated with a user in the DDS, "
                "make sure it is not misspelled."))

        user_email_str = user.primary_email
        current_user = auth.current_user()

        if current_user.role == "Unit Admin":
            if user.role not in ["Unit Admin", "Unit Personnel"]:
                raise ddserr.UserDeletionError(
                    message=
                    "You can only delete users with the role Unit Admin or Unit Personnel."
                )
            if current_user.unit != user.unit:
                raise ddserr.UserDeletionError(message=(
                    "As a Unit Admin, you're can only delete Unit Admins "
                    "and Unit Personnel within your specific unit."))

        if current_user == user:
            raise ddserr.UserDeletionError(
                message=
                "To delete your own account, use the '--self' flag instead!")

        self.delete_user(user)

        msg = (
            f"The user account {user.username} ({user_email_str}, {user.role}) has been "
            f"terminated successfully been by {current_user.name} ({current_user.role})."
        )
        flask.current_app.logger.info(msg)

        with structlog.threadlocal.bound_threadlocal(
                who={
                    "user": user.username,
                    "role": user.role
                },
                by_whom={
                    "user": current_user.username,
                    "role": current_user.role
                },
        ):
            action_logger.info(self.__class__)

        return {
            "message":
            (f"You successfully deleted the account {user.username} "
             f"({user_email_str}, {user.role})!")
        }
Beispiel #20
0
    def post(self):
        # Verify that user specified
        json_input = flask.request.json

        if "email" not in json_input:
            raise ddserr.DDSArgumentError(message="User email missing.")

        try:
            user = user_schemas.UserSchema().load(
                {"email": json_input.pop("email")})
        except sqlalchemy.exc.OperationalError as err:
            raise ddserr.DatabaseError(
                message=str(err), alt_message="Unexpected database error.")

        if not user:
            raise ddserr.NoSuchUserError()

        # Verify that the action is specified -- reactivate or deactivate
        action = json_input.get("action")
        if not action:
            raise ddserr.DDSArgumentError(
                message=
                "Please provide an action 'deactivate' or 'reactivate' for this request."
            )

        user_email_str = user.primary_email
        current_user = auth.current_user()

        if current_user.role == "Unit Admin":
            # Unit Admin can only activate/deactivate Unit Admins and personnel
            if user.role not in ["Unit Admin", "Unit Personnel"]:
                raise ddserr.AccessDeniedError(
                    message=("You can only activate/deactivate users with "
                             "the role Unit Admin or Unit Personnel."))

            if current_user.unit != user.unit:
                raise ddserr.AccessDeniedError(message=(
                    "As a Unit Admin, you can only activate/deactivate other Unit Admins or "
                    "Unit Personnel within your specific unit."))

        if current_user == user:
            raise ddserr.AccessDeniedError(
                message=f"You cannot {action} your own account!")

        if (action == "reactivate"
                and user.is_active) or (action == "deactivate"
                                        and not user.is_active):
            raise ddserr.DDSArgumentError(
                message=f"User is already {action}d!")

        # TODO: Check if user has lost access to any projects and if so, grant access again.
        if action == "reactivate":
            user.active = True

            # TODO: Super admins (current_user) don't have access to projects currently, how handle this?
            list_of_projects = None
            if user.role in ["Project Owner", "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

            from dds_web.api.project import ProjectAccess  # Needs to be here because of circ.import

            ProjectAccess.give_project_access(project_list=list_of_projects,
                                              current_user=current_user,
                                              user=user)

        else:
            user.active = False

        try:
            db.session.commit()
        except (sqlalchemy.exc.SQLAlchemyError,
                sqlalchemy.exc.OperationalError) as err:
            db.session.rollback()
            raise ddserr.DatabaseError(
                message=str(err),
                alt_message=f"Unexpected database error" +
                (": Database malfunction." if isinstance(
                    err, sqlalchemy.exc.OperationalError) else "."),
            ) from err
        msg = (
            f"The user account {user.username} ({user_email_str}, {user.role}) "
            f" has been {action}d successfully been by {current_user.name} ({current_user.role})."
        )
        flask.current_app.logger.info(msg)

        with structlog.threadlocal.bound_threadlocal(
                who={
                    "user": user.username,
                    "role": user.role
                },
                by_whom={
                    "user": current_user.username,
                    "role": current_user.role
                },
        ):
            action_logger.info(self.__class__)

        return {
            "message":
            (f"You successfully {action}d the account {user.username} "
             f"({user_email_str}, {user.role})!")
        }
Beispiel #21
0
 def get(self):
     """Get info regarding all projects which user is involved in."""
     return self.format_project_dict(current_user=auth.current_user())
Beispiel #22
0
    def delete(self):
        """Request deletion of own account."""
        current_user = auth.current_user()

        email_str = current_user.primary_email

        username = current_user.username

        proj_ids = None
        if current_user.role != "Super Admin":
            proj_ids = [proj.public_id for proj in current_user.projects]

        if current_user.role == "Unit Admin":
            num_admins = models.UnitUser.query.filter_by(
                unit_id=current_user.unit.id, is_admin=True).count()
            if num_admins <= 3:
                raise ddserr.AccessDeniedError(message=(
                    f"Your unit only has {num_admins} Unit Admins. "
                    "You cannot delete your account. "
                    "Invite a new Unit Admin first if you wish to proceed."))

        # Create URL safe token for invitation link
        s = itsdangerous.URLSafeTimedSerializer(
            flask.current_app.config["SECRET_KEY"])
        token = s.dumps(email_str, salt="email-delete")

        # Create deletion request in database unless it already exists
        try:
            if not dds_web.utils.delrequest_exists(email_str):
                new_delrequest = models.DeletionRequest(
                    **{
                        "requester": current_user,
                        "email": email_str,
                        "issued": dds_web.utils.current_time(),
                    })
                db.session.add(new_delrequest)
                db.session.commit()
            else:
                return {
                    "message": ("The confirmation link has already "
                                f"been sent to your address {email_str}!"),
                    "status":
                    http.HTTPStatus.OK,
                }

        except (sqlalchemy.exc.SQLAlchemyError,
                sqlalchemy.exc.OperationalError) as sqlerr:
            db.session.rollback()
            raise ddserr.DatabaseError(
                message=str(sqlerr),
                alt_message=f"Creation of self-deletion request failed" +
                (": Database malfunction." if isinstance(
                    sqlerr, sqlalchemy.exc.OperationalError) else "."),
            ) from sqlerr

        # Create link for deletion request email
        link = flask.url_for("auth_blueprint.confirm_self_deletion",
                             token=token,
                             _external=True)
        subject = f"Confirm deletion of your user account {username} in the SciLifeLab Data Delivery System"

        msg = flask_mail.Message(
            subject,
            recipients=[email_str],
        )

        # Need to attach the image to be able to use it
        msg.attach(
            "scilifelab_logo.png",
            "image/png",
            open(
                os.path.join(flask.current_app.static_folder,
                             "img/scilifelab_logo.png"), "rb").read(),
            "inline",
            headers=[
                ["Content-ID", "<Logo>"],
            ],
        )

        msg.body = flask.render_template(
            "mail/deletion_request.txt",
            link=link,
            sender_name=current_user.name,
            projects=proj_ids,
        )
        msg.html = flask.render_template(
            "mail/deletion_request.html",
            link=link,
            sender_name=current_user.name,
            projects=proj_ids,
        )

        mail.send(msg)

        flask.current_app.logger.info(
            f"The user account {username} / {email_str} ({current_user.role}) "
            "has requested self-deletion.")

        return {
            "message":
            ("Requested account deletion initiated. An e-mail with a "
             f"confirmation link has been sent to your address {email_str}!"),
        }
Beispiel #23
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,
        }
Beispiel #24
0
    def compose_and_send_email_to_user(userobj,
                                       mail_type,
                                       link=None,
                                       project=None):
        """Compose and send email"""
        if hasattr(userobj, "emails"):
            recipients = [x.email for x in userobj.emails]
        else:
            # userobj likely an invite
            recipients = [userobj.email]

        unit_name = None
        unit_email = None
        project_id = None
        deadline = None
        if auth.current_user().role in ["Unit Admin", "Unit Personnel"]:
            unit = auth.current_user().unit
            unit_name = unit.external_display_name
            unit_email = unit.contact_email
            sender_name = auth.current_user().name
            subject_subject = unit_name

        else:
            sender_name = auth.current_user().name
            subject_subject = sender_name

        # Fill in email subject with sentence subject
        if mail_type == "invite":
            subject = f"{subject_subject} invites you to the SciLifeLab Data Delivery System"
        elif mail_type == "project_release":
            subject = f"Project made available by {subject_subject} in the SciLifeLab Data Delivery System"
            project_id = project.public_id
            deadline = project.current_deadline.astimezone(
                datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S %Z")
        else:
            raise ddserr.DDSArgumentError(message="Invalid mail type!")

        msg = flask_mail.Message(
            subject,
            recipients=recipients,
        )

        # Need to attach the image to be able to use it
        msg.attach(
            "scilifelab_logo.png",
            "image/png",
            open(
                os.path.join(flask.current_app.static_folder,
                             "img/scilifelab_logo.png"), "rb").read(),
            "inline",
            headers=[
                ["Content-ID", "<Logo>"],
            ],
        )

        msg.body = flask.render_template(
            f"mail/{mail_type}.txt",
            link=link,
            sender_name=sender_name,
            unit_name=unit_name,
            unit_email=unit_email,
            project_id=project_id,
            deadline=deadline,
        )
        msg.html = flask.render_template(
            f"mail/{mail_type}.html",
            link=link,
            sender_name=sender_name,
            unit_name=unit_name,
            unit_email=unit_email,
            project_id=project_id,
            deadline=deadline,
        )

        AddUser.send_email_with_retry(msg)
Beispiel #25
0
    def add_to_project(whom, project, role, send_email=True):
        """Add existing user or invite to a project"""

        allowed_roles = ["Project Owner", "Researcher"]

        if role not in allowed_roles:
            return {
                "status":
                ddserr.AccessDeniedError.code.value,
                "message": ("User Role should be either 'Project Owner' or "
                            "'Researcher' to be added to a project"),
            }

        if whom.role not in allowed_roles:
            return {
                "status":
                ddserr.AccessDeniedError.code.value,
                "message":
                ("Users affiliated with units can not be added to projects individually."
                 ),
            }

        is_owner = role == "Project Owner"
        ownership_change = False

        if isinstance(whom, models.ResearchUser):
            project_user_row = models.ProjectUsers.query.filter_by(
                project_id=project.id, user_id=whom.username).one_or_none()
        else:
            project_user_row = models.ProjectInviteKeys.query.filter_by(
                project_id=project.id, invite_id=whom.id).one_or_none()

        if project_user_row:
            send_email = False
            if project_user_row.owner == is_owner:
                return {
                    "status":
                    ddserr.RoleException.code.value,
                    "message":
                    f"{str(whom)} is already associated with the {str(project)} in this capacity. ",
                }
            ownership_change = True
            project_user_row.owner = is_owner

        if not ownership_change:
            if isinstance(whom, models.ResearchUser):
                project.researchusers.append(
                    models.ProjectUsers(
                        project_id=project.id,
                        user_id=whom.username,
                        owner=is_owner,
                    ))

            try:
                share_project_private_key(
                    from_user=auth.current_user(),
                    to_another=whom,
                    from_user_token=dds_web.security.auth.
                    obtain_current_encrypted_token(),
                    project=project,
                    is_project_owner=is_owner,
                )
            except ddserr.KeyNotFoundError as keyerr:
                return {
                    "message":
                    ("You do not have access to the current project. To get access, "
                     "ask the a user within the responsible unit to grant you access."
                     ),
                    "status":
                    ddserr.AccessDeniedError.code.value,
                }

        try:
            db.session.commit()
        except (
                sqlalchemy.exc.SQLAlchemyError,
                sqlalchemy.exc.IntegrityError,
                sqlalchemy.exc.OperationalError,
        ) as err:
            flask.current_app.logger.exception(err)
            db.session.rollback()
            raise ddserr.DatabaseError(
                message=str(err),
                alt_message=
                f"Server Error: User was not associated with the project" +
                (": Database malfunction." if isinstance(
                    err, sqlalchemy.exc.OperationalError) else "."),
            ) from err

        # If project is already released and not expired, send mail to user
        send_email = send_email and project.current_status == "Available"
        if send_email:
            AddUser.compose_and_send_email_to_user(whom,
                                                   "project_release",
                                                   project=project)

        flask.current_app.logger.debug(
            f"{str(whom)} was given access to the {str(project)} as a {'Project Owner' if is_owner else 'Researcher'}."
        )

        return {
            "status":
            http.HTTPStatus.OK,
            "message":
            (f"{str(whom)} was given access to the "
             f"{str(project)} as a {'Project Owner' if is_owner else 'Researcher'}. An e-mail notification has{' not ' if not send_email else ' '}been sent."
             ),
        }
Beispiel #26
0
def add_before_project_update(mapper, connection, target):
    """Listen for the 'before_update' event on Project and update certain of its fields"""
    if auth.current_user():
        target.date_updated = dds_web.utils.current_time()
        target.last_updated_by = auth.current_user().username