示例#1
0
    def test_accept_invite(self, get_audit, create_audit):
        om = OrganizationMember.objects.get(id=self.member.id)
        assert om.email == self.member.email

        helper = ApiInviteHelper(self.request, self.member.id, None)
        helper.accept_invite()

        om = OrganizationMember.objects.get(id=self.member.id)
        assert om.email is None
        assert om.user.id == self.user.id
示例#2
0
    def test_accept_invite_with_SSO(self, mock_provider, get_audit, create_audit):
        self.auth_provider.flags.allow_unlinked = True
        mock_provider.get.return_value = self.auth_provider

        om = OrganizationMember.objects.get(id=self.member.id)
        assert om.email == self.member.email

        helper = ApiInviteHelper(self.request, self.member.id, None)
        helper.accept_invite()

        om = OrganizationMember.objects.get(id=self.member.id)
        assert om.email is None
        assert om.user.id == self.user.id
示例#3
0
    def test_accept_invite_with_required_SSO(self, mock_provider, get_audit, create_audit):
        self.auth_provider.flags.allow_unlinked = False
        mock_provider.get.return_value = self.auth_provider

        om = OrganizationMember.objects.get(id=self.member.id)
        assert om.email == self.member.email

        helper = ApiInviteHelper(self.request, self.member.id, None)
        helper.accept_invite()

        # Invite cannot be accepted without AuthIdentity if SSO is required
        om = OrganizationMember.objects.get(id=self.member.id)
        assert om.email is not None
        assert om.user is None
示例#4
0
    def _handle_new_membership(
            self, auth_identity: AuthIdentity) -> Optional[OrganizationMember]:
        user = auth_identity.user

        # If the user is either currently *pending* invite acceptance (as indicated
        # from the pending-invite cookie) OR an existing invite exists on this
        # organziation for the email provided by the identity provider.
        invite_helper = ApiInviteHelper.from_cookie_or_email(
            request=self.request,
            organization=self.organization,
            email=user.email)

        # If we are able to accept an existing invite for the user for this
        # organization, do so, otherwise handle new membership
        if invite_helper:
            if invite_helper.invite_approved:
                return invite_helper.accept_invite(user)

            # It's possible the user has an _invite request_ that hasn't been approved yet,
            # and is able to join the organization without an invite through the SSO flow.
            # In that case, delete the invite request and create a new membership.
            invite_helper.handle_invite_not_approved()

        flags = OrganizationMember.flags["sso:linked"]
        # if the org doesn't have the ability to add members then anyone who got added
        # this way should be disabled until the org upgrades
        if not features.has("organizations:invite-members", self.organization):
            flags = flags | OrganizationMember.flags["member-limit:restricted"]

        # Otherwise create a new membership
        om = OrganizationMember.objects.create(
            organization=self.organization,
            role=self.organization.default_role,
            user=user,
            flags=flags,
        )

        default_teams = self.auth_provider.default_teams.all()
        for team in default_teams:
            OrganizationMemberTeam.objects.create(team=team,
                                                  organizationmember=om)

        AuditLogEntry.objects.create(
            organization=self.organization,
            actor=user,
            ip_address=self.request.META["REMOTE_ADDR"],
            target_object=om.id,
            target_user=om.user,
            event=AuditLogEntryEvent.MEMBER_ADD,
            data=om.get_audit_log_data(),
        )

        return om
示例#5
0
def handle_new_membership(auth_provider, organization, request, auth_identity):
    user = auth_identity.user

    # If the user is either currently *pending* invite acceptance (as indicated
    # from the pending-invite cookie) OR an existing invite exists on this
    # organziation for the email provided by the identity provider.
    invite_helper = ApiInviteHelper.from_cookie_or_email(
        request=request, organization=organization, email=user.email
    )

    # If we are able to accept an existing invite for the user for this
    # organization, do so, otherwise handle new membership
    if invite_helper:
        if invite_helper.invite_approved:
            invite_helper.accept_invite(user)
            return

        # It's possible the user has an _invite request_ that hasn't been approved yet,
        # and is able to join the organization without an invite through the SSO flow.
        # In that case, delete the invite request and create a new membership.
        invite_helper.handle_invite_not_approved()

    # Otherwise create a new membership
    om = OrganizationMember.objects.create(
        organization=organization,
        role=organization.default_role,
        user=user,
        flags=OrganizationMember.flags["sso:linked"],
    )

    default_teams = auth_provider.default_teams.all()
    for team in default_teams:
        OrganizationMemberTeam.objects.create(team=team, organizationmember=om)

    AuditLogEntry.objects.create(
        organization=organization,
        actor=user,
        ip_address=request.META["REMOTE_ADDR"],
        target_object=om.id,
        target_user=om.user,
        event=AuditLogEntryEvent.MEMBER_ADD,
        data=om.get_audit_log_data(),
    )

    return om
    def post(self, request, user, interface_id):
        """
        Enroll in authenticator interface
        `````````````````````````````````

        :pparam string user_id: user id or "me" for current user
        :pparam string interface_id: interface id

        :auth: required
        """
        if ratelimiter.is_limited(
                f"auth:authenticator-enroll:{request.user.id}:{interface_id}",
                limit=10,
                window=86400,  # 10 per day should be fine
        ):
            return HttpResponse(
                "You have made too many authenticator enrollment attempts. Please try again later.",
                content_type="text/plain",
                status=429,
            )

        # Using `request.user` here because superuser should not be able to set a user's 2fa

        # start activation
        serializer_cls = serializer_map.get(interface_id, None)

        if serializer_cls is None:
            return Response(status=status.HTTP_404_NOT_FOUND)

        serializer = serializer_cls(data=request.data)

        if not serializer.is_valid():
            return Response(serializer.errors,
                            status=status.HTTP_400_BAD_REQUEST)

        interface = Authenticator.objects.get_interface(
            request.user, interface_id)

        # Not all interfaces allow multi enrollment
        #
        # This is probably un-needed because we catch
        # `Authenticator.AlreadyEnrolled` when attempting to enroll
        if interface.is_enrolled() and not interface.allow_multi_enrollment:
            return Response(ALREADY_ENROLLED_ERR,
                            status=status.HTTP_400_BAD_REQUEST)

        try:
            interface.secret = request.data["secret"]
        except KeyError:
            pass

        context = {}
        # Need to update interface with phone number before validating OTP
        if "phone" in request.data:
            interface.phone_number = serializer.data["phone"]

            # Disregarding value of 'otp', if no OTP was provided,
            # send text message to phone number with OTP
            if "otp" not in request.data:
                if interface.send_text(for_enrollment=True,
                                       request=request._request):
                    return Response(status=status.HTTP_204_NO_CONTENT)
                else:
                    # Error sending text message
                    return Response(
                        SEND_SMS_ERR,
                        status=status.HTTP_500_INTERNAL_SERVER_ERROR)

        # Attempt to validate OTP
        if "otp" in request.data and not interface.validate_otp(
                serializer.data["otp"]):
            return Response(INVALID_OTP_ERR,
                            status=status.HTTP_400_BAD_REQUEST)

        # Try u2f enrollment
        if interface_id == "u2f":
            # What happens when this fails?
            interface.try_enroll(
                serializer.data["challenge"],
                serializer.data["response"],
                serializer.data["deviceName"],
            )
            context.update({"device_name": serializer.data["deviceName"]})

        try:
            interface.enroll(request.user)
        except Authenticator.AlreadyEnrolled:
            return Response(ALREADY_ENROLLED_ERR,
                            status=status.HTTP_400_BAD_REQUEST)

        context.update({"authenticator": interface.authenticator})
        capture_security_activity(
            account=request.user,
            type="mfa-added",
            actor=request.user,
            ip_address=request.META["REMOTE_ADDR"],
            context=context,
            send_email=True,
        )
        request.user.clear_lost_passwords()
        request.user.refresh_session_nonce(self.request)
        request.user.save()
        Authenticator.objects.auto_add_recovery_codes(request.user)

        response = Response(status=status.HTTP_204_NO_CONTENT)

        # If there is a pending organization invite accept after the
        # authenticator has been configured.
        invite_helper = ApiInviteHelper.from_cookie(request=request,
                                                    instance=self,
                                                    logger=logger)

        if invite_helper and invite_helper.valid_request:
            invite_helper.accept_invite()
            remove_invite_cookie(request, response)

        return response
 def get_helper(self, request, member_id, token):
     return ApiInviteHelper(request=request,
                            member_id=member_id,
                            instance=self,
                            token=token)
示例#8
0
    def handle_basic_auth(self, request, **kwargs):
        can_register = self.can_register(request)

        op = request.POST.get("op")
        organization = kwargs.pop("organization", None)

        if not op:
            # Detect that we are on the register page by url /register/ and
            # then activate the register tab by default.
            if "/register" in request.path_info and can_register:
                op = "register"
            elif request.GET.get("op") == "sso":
                op = "sso"

        login_form = self.get_login_form(request)
        if can_register:
            register_form = self.get_register_form(
                request, initial={"username": request.session.get("invite_email", "")}
            )
        else:
            register_form = None

        if can_register and register_form.is_valid():
            user = register_form.save()
            user.send_confirm_emails(is_new_user=True)
            user_signup.send_robust(
                sender=self, user=user, source="register-form", referrer="in-app"
            )

            # HACK: grab whatever the first backend is and assume it works
            user.backend = settings.AUTHENTICATION_BACKENDS[0]

            auth.login(request, user, organization_id=organization.id if organization else None)

            # can_register should only allow a single registration
            request.session.pop("can_register", None)
            request.session.pop("invite_email", None)

            # In single org mode, associate the user to the orgnaization
            if settings.SENTRY_SINGLE_ORGANIZATION:
                organization = Organization.get_default()
                OrganizationMember.objects.create(
                    organization=organization, role=organization.default_role, user=user
                )

            # Attempt to directly accept any pending invites
            invite_helper = ApiInviteHelper.from_cookie(request=request, instance=self)

            if invite_helper and invite_helper.valid_request:
                invite_helper.accept_invite()
                response = self.redirect_to_org(request)
                remove_invite_cookie(request, response)

                return response

            return self.redirect(auth.get_login_redirect(request))

        elif request.method == "POST":
            from sentry.app import ratelimiter
            from sentry.utils.hashlib import md5_text

            login_attempt = (
                op == "login" and request.POST.get("username") and request.POST.get("password")
            )

            if login_attempt and ratelimiter.is_limited(
                u"auth:login:username:{}".format(
                    md5_text(login_form.clean_username(request.POST["username"])).hexdigest()
                ),
                limit=10,
                window=60,  # 10 per minute should be enough for anyone
            ):
                login_form.errors["__all__"] = [
                    u"You have made too many login attempts. Please try again later."
                ]
                metrics.incr(
                    "login.attempt", instance="rate_limited", skip_internal=True, sample_rate=1.0
                )
            elif login_form.is_valid():
                user = login_form.get_user()

                auth.login(request, user, organization_id=organization.id if organization else None)
                metrics.incr(
                    "login.attempt", instance="success", skip_internal=True, sample_rate=1.0
                )

                if not user.is_active:
                    return self.redirect(reverse("sentry-reactivate-account"))

                return self.redirect(auth.get_login_redirect(request))
            else:
                metrics.incr(
                    "login.attempt", instance="failure", skip_internal=True, sample_rate=1.0
                )

        context = {
            "op": op or "login",
            "server_hostname": get_server_hostname(),
            "login_form": login_form,
            "organization": organization,
            "register_form": register_form,
            "CAN_REGISTER": can_register,
            "join_request_link": self.get_join_request_link(organization),
        }
        context.update(additional_context.run_callbacks(request))

        return self.respond_login(request, context, **kwargs)
    def post(self, request, user, interface_id):
        """
        Enroll in authenticator interface
        `````````````````````````````````

        :pparam string user_id: user id or "me" for current user
        :pparam string interface_id: interface id

        :auth: required
        """

        # Using `request.user` here because superuser should not be able to set a user's 2fa

        # start activation
        serializer_cls = serializer_map.get(interface_id, None)

        if serializer_cls is None:
            return Response(status=status.HTTP_404_NOT_FOUND)

        serializer = serializer_cls(data=request.data)

        if not serializer.is_valid():
            return Response(serializer.errors,
                            status=status.HTTP_400_BAD_REQUEST)

        interface = Authenticator.objects.get_interface(
            request.user, interface_id)

        # Not all interfaces allow multi enrollment
        #
        # This is probably un-needed because we catch
        # `Authenticator.AlreadyEnrolled` when attempting to enroll
        if interface.is_enrolled and not interface.allow_multi_enrollment:
            return Response(ALREADY_ENROLLED_ERR,
                            status=status.HTTP_400_BAD_REQUEST)

        try:
            interface.secret = request.data["secret"]
        except KeyError:
            pass

        context = {}
        # Need to update interface with phone number before validating OTP
        if "phone" in request.data:
            interface.phone_number = serializer.data["phone"]

            # Disregarding value of 'otp', if no OTP was provided,
            # send text message to phone number with OTP
            if "otp" not in request.data:
                if interface.send_text(for_enrollment=True,
                                       request=request._request):
                    return Response(status=status.HTTP_204_NO_CONTENT)
                else:
                    # Error sending text message
                    return Response(
                        SEND_SMS_ERR,
                        status=status.HTTP_500_INTERNAL_SERVER_ERROR)

        # Attempt to validate OTP
        if "otp" in request.data and not interface.validate_otp(
                serializer.data["otp"]):
            return Response(INVALID_OTP_ERR,
                            status=status.HTTP_400_BAD_REQUEST)

        # Try u2f enrollment
        if interface_id == "u2f":
            # What happens when this fails?
            interface.try_enroll(
                serializer.data["challenge"],
                serializer.data["response"],
                serializer.data["deviceName"],
            )
            context.update({"device_name": serializer.data["deviceName"]})

        try:
            interface.enroll(request.user)
        except Authenticator.AlreadyEnrolled:
            return Response(ALREADY_ENROLLED_ERR,
                            status=status.HTTP_400_BAD_REQUEST)
        else:
            context.update({"authenticator": interface.authenticator})
            capture_security_activity(
                account=request.user,
                type="mfa-added",
                actor=request.user,
                ip_address=request.META["REMOTE_ADDR"],
                context=context,
                send_email=True,
            )
            request.user.clear_lost_passwords()
            request.user.refresh_session_nonce(self.request)
            request.user.save()
            Authenticator.objects.auto_add_recovery_codes(request.user)

            # Try to accept an org invite pending 2FA enrollment
            member_id = serializer.data.get("memberId")
            token = serializer.data.get("token")

            if member_id and token:
                try:
                    helper = ApiInviteHelper(
                        instance=self,
                        request=request,
                        member_id=member_id,
                        token=token,
                        logger=logger,
                    )
                except OrganizationMember.DoesNotExist:
                    logger.error("Failed to accept pending org invite",
                                 exc_info=True)
                else:
                    if helper.valid_request:
                        helper.accept_invite()

                        response = Response(status=status.HTTP_204_NO_CONTENT)
                        helper.remove_invite_cookie(response)
                        return response

            return Response(status=status.HTTP_204_NO_CONTENT)