Exemple #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"]
Exemple #2
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}
Exemple #3
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.")
Exemple #4
0
    def generate_required_fields(self, data, **kwargs):
        """Generate all required fields for creating a project."""
        if not data:
            raise ddserr.DDSArgumentError(
                "No project information found when attempting to create project."
            )

        data["date_created"] = dds_web.utils.current_time()

        return data
Exemple #5
0
    def post(self):
        """Add a MOTD."""

        curr_date = utils.current_time()
        json_input = flask.request.json
        motd = json_input.get("message")
        if not motd:
            raise ddserr.DDSArgumentError(message="No MOTD specified.")

        flask.current_app.logger.debug(motd)
        new_motd = models.MOTD(message=motd, date_created=curr_date)
        db.session.add(new_motd)
        db.session.commit()
Exemple #6
0
    def return_items(self, data, **kwargs):
        """Return project contents as serialized."""

        # Fields
        requested_items = data.get("requested_items")
        url = data.get("url")
        get_all = data.get("get_all")

        # Check if project has contents
        project_row = verify_project_exists(spec_proj=data.get("project"))
        if not project_row.files:
            raise ddserr.EmptyProjectException(project=project_row.public_id)

        # Check if specific files have been requested or if requested all contents
        files, folder_contents, not_found = (None, None, None)
        if requested_items:
            files, folder_contents, not_found = self.find_contents(
                project=project_row, contents=requested_items)
        elif get_all:
            files = project_row.files
        else:
            raise ddserr.DDSArgumentError(message="No items were requested.")

        # Items to return
        found_files = {}
        found_folder_contents = {}
        not_found = {}

        # Use file schema to get file info automatically
        fileschema = sqlalchemyautoschemas.FileSchema(
            many=False,
            only=(
                "name_in_bucket",
                "subpath",
                "size_original",
                "size_stored",
                "salt",
                "public_key",
                "checksum",
                "compressed",
            ),
        )

        # Connect to s3
        with api_s3_connector.ApiS3Connector(project=project_row) as s3:
            # Get the info and signed urls for all files
            try:
                found_files.update({
                    x.name: {
                        **fileschema.dump(x),
                        "url":
                        s3.generate_get_url(
                            key=x.name_in_bucket) if url else None,
                    }
                    for x in files
                })

                if folder_contents:
                    # Get all info and signed urls for all folder contents found in the bucket
                    for x, y in folder_contents.items():
                        if x not in found_folder_contents:
                            found_folder_contents[x] = {}

                        found_folder_contents[x].update({
                            z.name: {
                                **fileschema.dump(z),
                                "url":
                                s3.generate_get_url(
                                    key=z.name_in_bucket) if url else None,
                            }
                            for z in y
                        })
            except botocore.client.ClientError as clierr:
                raise ddserr.S3ConnectionError(
                    message=str(clierr),
                    alt_message="Could not generate presigned urls.")

        return found_files, found_folder_contents, not_found
Exemple #7
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})!")
        }
Exemple #8
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)
Exemple #9
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,
        }