예제 #1
0
def rate_limit_user(request: HttpRequest, user: UserProfile, domain: str) -> None:
    """Returns whether or not a user was rate limited. Will raise a RateLimited exception
    if the user has been rate limited, otherwise returns and modifies request to contain
    the rate limit information"""

    entity = RateLimitedUser(user, domain=domain)
    ratelimited, time = is_ratelimited(entity)
    request._ratelimit_applied_limits = True
    request._ratelimit_secs_to_freedom = time
    request._ratelimit_over_limit = ratelimited
    # Abort this request if the user is over their rate limits
    if ratelimited:
        statsd.incr("ratelimiter.limited.%s.%s" % (type(user), user.id))
        raise RateLimited()

    try:
        incr_ratelimit(entity)
    except RateLimiterLockingException:
        logging.warning("Deadlock trying to incr_ratelimit for %s on %s" % (
            user.id, request.path))
        # rate-limit users who are hitting the API so hard we can't update our stats.
        raise RateLimited()

    calls_remaining, time_reset = api_calls_left(entity)

    request._ratelimit_remaining = calls_remaining
    request._ratelimit_secs_to_freedom = time_reset
예제 #2
0
def rate_limit_mirror_by_realm(recipient_realm: Realm) -> None:
    # Code based on the rate_limit_user function:
    entity = RateLimitedRealmMirror(recipient_realm)
    ratelimited, time = is_ratelimited(entity)

    if ratelimited:
        statsd.incr("ratelimiter.limited.%s.%s" %
                    (type(recipient_realm), recipient_realm.id))
        raise RateLimited()

    try:
        incr_ratelimit(entity)
    except RateLimiterLockingException:
        logger.warning("Email mirror rate limiter: Deadlock trying to "
                       "incr_ratelimit for realm %s" %
                       (recipient_realm.name, ))
        raise RateLimited()
예제 #3
0
    def rate_limit_request(self, request: HttpRequest) -> None:
        ratelimited, time = self.rate_limit()

        entity_type = type(self).__name__
        if not hasattr(request, '_ratelimit'):
            request._ratelimit = {}
        request._ratelimit[entity_type] = RateLimitResult(
            entity=self, secs_to_freedom=time, over_limit=ratelimited)
        # Abort this request if the user is over their rate limits
        if ratelimited:
            # Pass information about what kind of entity got limited in the exception:
            raise RateLimited(entity_type)

        calls_remaining, time_reset = self.api_calls_left()

        request._ratelimit[entity_type].remaining = calls_remaining
        request._ratelimit[entity_type].secs_to_freedom = time_reset
예제 #4
0
    def rate_limit_request(self, request: HttpRequest) -> None:
        ratelimited, time = self.rate_limit()

        if not hasattr(request, '_ratelimits_applied'):
            request._ratelimits_applied = []
        request._ratelimits_applied.append(
            RateLimitResult(entity=self,
                            secs_to_freedom=time,
                            remaining=0,
                            over_limit=ratelimited))
        # Abort this request if the user is over their rate limits
        if ratelimited:
            # Pass information about what kind of entity got limited in the exception:
            raise RateLimited(str(time))

        calls_remaining, seconds_until_reset = self.api_calls_left()

        request._ratelimits_applied[-1].remaining = calls_remaining
        request._ratelimits_applied[-1].secs_to_freedom = seconds_until_reset
예제 #5
0
def rate_limit_user(request: HttpRequest, user: UserProfile,
                    domain: str) -> None:
    """Returns whether or not a user was rate limited. Will raise a RateLimited exception
    if the user has been rate limited, otherwise returns and modifies request to contain
    the rate limit information"""

    entity = RateLimitedUser(user, domain=domain)
    ratelimited, time = rate_limit_entity(entity)
    request._ratelimit_applied_limits = True
    request._ratelimit_secs_to_freedom = time
    request._ratelimit_over_limit = ratelimited
    # Abort this request if the user is over their rate limits
    if ratelimited:
        raise RateLimited()

    calls_remaining, time_reset = api_calls_left(entity)

    request._ratelimit_remaining = calls_remaining
    request._ratelimit_secs_to_freedom = time_reset
예제 #6
0
def rate_limit_request_by_entity(request: HttpRequest,
                                 entity: RateLimitedObject) -> None:
    ratelimited, time = rate_limit_entity(entity)

    entity_type = type(entity).__name__
    if not hasattr(request, '_ratelimit'):
        request._ratelimit = {}
    request._ratelimit[entity_type] = {}
    request._ratelimit[entity_type]['applied_limits'] = True
    request._ratelimit[entity_type]['secs_to_freedom'] = time
    request._ratelimit[entity_type]['over_limit'] = ratelimited
    # Abort this request if the user is over their rate limits
    if ratelimited:
        # Pass information about what kind of entity got limited in the exception:
        raise RateLimited(entity_type)

    calls_remaining, time_reset = api_calls_left(entity)

    request._ratelimit[entity_type]['remaining'] = calls_remaining
    request._ratelimit[entity_type]['secs_to_freedom'] = time_reset
예제 #7
0
def rate_limit_user(request, user, domain):
    # type: (HttpRequest, UserProfile, Text) -> None
    """Returns whether or not a user was rate limited. Will raise a RateLimited exception
    if the user has been rate limited, otherwise returns and modifies request to contain
    the rate limit information"""

    ratelimited, time = is_ratelimited(user, domain)
    request._ratelimit_applied_limits = True
    request._ratelimit_secs_to_freedom = time
    request._ratelimit_over_limit = ratelimited
    # Abort this request if the user is over their rate limits
    if ratelimited:
        statsd.incr("ratelimiter.limited.%s.%s" % (type(user), user.id))
        raise RateLimited()

    incr_ratelimit(user, domain)
    calls_remaining, time_reset = api_calls_left(user, domain)

    request._ratelimit_remaining = calls_remaining
    request._ratelimit_secs_to_freedom = time_reset
예제 #8
0
    def rate_limit_request(self, request: HttpRequest) -> None:
        from zerver.lib.request import RequestNotes

        ratelimited, time = self.rate_limit()
        request_notes = RequestNotes.get_notes(request)

        request_notes.ratelimits_applied.append(
            RateLimitResult(
                entity=self,
                secs_to_freedom=time,
                remaining=0,
                over_limit=ratelimited,
            ))
        # Abort this request if the user is over their rate limits
        if ratelimited:
            # Pass information about what kind of entity got limited in the exception:
            raise RateLimited(time)

        calls_remaining, seconds_until_reset = self.api_calls_left()

        request_notes.ratelimits_applied[-1].remaining = calls_remaining
        request_notes.ratelimits_applied[
            -1].secs_to_freedom = seconds_until_reset
예제 #9
0
def rate_limit_mirror_by_realm(recipient_realm: Realm) -> None:
    entity = RateLimitedRealmMirror(recipient_realm)
    ratelimited = rate_limit_entity(entity)[0]

    if ratelimited:
        raise RateLimited()
예제 #10
0
def rate_limit_mirror_by_realm(recipient_realm: Realm) -> None:
    ratelimited = RateLimitedRealmMirror(recipient_realm).rate_limit()[0]

    if ratelimited:
        raise RateLimited()
예제 #11
0
def rate_limit_mirror_by_realm(recipient_realm: Realm) -> None:
    ratelimited, secs_to_freedom = RateLimitedRealmMirror(
        recipient_realm).rate_limit()

    if ratelimited:
        raise RateLimited(secs_to_freedom)
예제 #12
0
def json_change_settings(
    request: HttpRequest,
    user_profile: UserProfile,
    full_name: str = REQ(default=""),
    email: str = REQ(default=""),
    old_password: str = REQ(default=""),
    new_password: str = REQ(default=""),
    twenty_four_hour_time: Optional[bool] = REQ(json_validator=check_bool,
                                                default=None),
    dense_mode: Optional[bool] = REQ(json_validator=check_bool, default=None),
    starred_message_counts: Optional[bool] = REQ(json_validator=check_bool,
                                                 default=None),
    fluid_layout_width: Optional[bool] = REQ(json_validator=check_bool,
                                             default=None),
    high_contrast_mode: Optional[bool] = REQ(json_validator=check_bool,
                                             default=None),
    color_scheme: Optional[int] = REQ(json_validator=check_int_in(
        UserProfile.COLOR_SCHEME_CHOICES),
                                      default=None),
    translate_emoticons: Optional[bool] = REQ(json_validator=check_bool,
                                              default=None),
    default_language: Optional[str] = REQ(default=None),
    default_view: Optional[str] = REQ(
        str_validator=check_string_in(default_view_options), default=None),
    escape_navigates_to_default_view: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    left_side_userlist: Optional[bool] = REQ(json_validator=check_bool,
                                             default=None),
    emojiset: Optional[str] = REQ(
        str_validator=check_string_in(emojiset_choices), default=None),
    demote_inactive_streams: Optional[int] = REQ(json_validator=check_int_in(
        UserProfile.DEMOTE_STREAMS_CHOICES),
                                                 default=None),
    timezone: Optional[str] = REQ(str_validator=check_string_in(
        pytz.all_timezones_set),
                                  default=None),
    email_notifications_batching_period_seconds: Optional[int] = REQ(
        json_validator=check_int, default=None),
    enable_drafts_synchronization: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    enable_stream_desktop_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    enable_stream_email_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    enable_stream_push_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    enable_stream_audible_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    wildcard_mentions_notify: Optional[bool] = REQ(json_validator=check_bool,
                                                   default=None),
    notification_sound: Optional[str] = REQ(default=None),
    enable_desktop_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    enable_sounds: Optional[bool] = REQ(json_validator=check_bool,
                                        default=None),
    enable_offline_email_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    enable_offline_push_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    enable_online_push_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    enable_digest_emails: Optional[bool] = REQ(json_validator=check_bool,
                                               default=None),
    enable_login_emails: Optional[bool] = REQ(json_validator=check_bool,
                                              default=None),
    enable_marketing_emails: Optional[bool] = REQ(json_validator=check_bool,
                                                  default=None),
    message_content_in_email_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    pm_content_in_desktop_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    desktop_icon_count_display: Optional[int] = REQ(
        json_validator=check_int_in(
            UserProfile.DESKTOP_ICON_COUNT_DISPLAY_CHOICES),
        default=None),
    realm_name_in_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    presence_enabled: Optional[bool] = REQ(json_validator=check_bool,
                                           default=None),
    enter_sends: Optional[bool] = REQ(json_validator=check_bool, default=None),
    send_private_typing_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    send_stream_typing_notifications: Optional[bool] = REQ(
        json_validator=check_bool, default=None),
    send_read_receipts: Optional[bool] = REQ(json_validator=check_bool,
                                             default=None),
) -> HttpResponse:
    if (default_language is not None or notification_sound is not None
            or email_notifications_batching_period_seconds is not None):
        check_settings_values(notification_sound,
                              email_notifications_batching_period_seconds,
                              default_language)

    if new_password != "":
        return_data: Dict[str, Any] = {}
        if email_belongs_to_ldap(user_profile.realm,
                                 user_profile.delivery_email):
            raise JsonableError(_("Your Zulip password is managed in LDAP"))

        try:
            if not authenticate(
                    request,
                    username=user_profile.delivery_email,
                    password=old_password,
                    realm=user_profile.realm,
                    return_data=return_data,
            ):
                raise JsonableError(_("Wrong password!"))
        except RateLimited as e:
            assert e.secs_to_freedom is not None
            secs_to_freedom = int(e.secs_to_freedom)
            raise JsonableError(
                _("You're making too many attempts! Try again in {} seconds.").
                format(secs_to_freedom), )

        if not check_password_strength(new_password):
            raise JsonableError(_("New password is too weak!"))

        do_change_password(user_profile, new_password)
        # Password changes invalidates sessions, see
        # https://docs.djangoproject.com/en/3.2/topics/auth/default/#session-invalidation-on-password-change
        # for details. To avoid this logging the user out of their own
        # session (which would provide a confusing UX at best), we
        # update the session hash here.
        update_session_auth_hash(request, user_profile)
        # We also save the session to the DB immediately to mitigate
        # race conditions. In theory, there is still a race condition
        # and to completely avoid it we will have to use some kind of
        # mutex lock in `django.contrib.auth.get_user` where session
        # is verified. To make that lock work we will have to control
        # the AuthenticationMiddleware which is currently controlled
        # by Django,
        request.session.save()

    result: Dict[str, Any] = {}
    new_email = email.strip()
    if user_profile.delivery_email != new_email and new_email != "":
        if user_profile.realm.email_changes_disabled and not user_profile.is_realm_admin:
            raise JsonableError(
                _("Email address changes are disabled in this organization."))

        error = validate_email_is_valid(
            new_email,
            get_realm_email_validator(user_profile.realm),
        )
        if error:
            raise JsonableError(error)

        try:
            validate_email_not_already_in_realm(
                user_profile.realm,
                new_email,
                verbose=False,
            )
        except ValidationError as e:
            raise JsonableError(e.message)

        ratelimited, time_until_free = RateLimitedUser(
            user_profile, domain="email_change_by_user").rate_limit()
        if ratelimited:
            raise RateLimited(time_until_free)

        do_start_email_change_process(user_profile, new_email)

    if user_profile.full_name != full_name and full_name.strip() != "":
        if name_changes_disabled(
                user_profile.realm) and not user_profile.is_realm_admin:
            # Failingly silently is fine -- they can't do it through the UI, so
            # they'd have to be trying to break the rules.
            pass
        else:
            # Note that check_change_full_name strips the passed name automatically
            check_change_full_name(user_profile, full_name, user_profile)

    # Loop over user_profile.property_types
    request_settings = {
        k: v
        for k, v in list(locals().items()) if k in user_profile.property_types
    }
    for k, v in list(request_settings.items()):
        if v is not None and getattr(user_profile, k) != v:
            do_change_user_setting(user_profile,
                                   k,
                                   v,
                                   acting_user=user_profile)

    if timezone is not None and user_profile.timezone != timezone:
        do_change_user_setting(user_profile,
                               "timezone",
                               timezone,
                               acting_user=user_profile)

    # TODO: Do this more generally.
    from zerver.lib.request import RequestNotes

    request_notes = RequestNotes.get_notes(request)
    for req_var in request.POST:
        if req_var not in request_notes.processed_parameters:
            request_notes.ignored_parameters.add(req_var)

    if len(request_notes.ignored_parameters) > 0:
        result["ignored_parameters_unsupported"] = list(
            request_notes.ignored_parameters)

    return json_success(result)
예제 #13
0
def avatar(
    request: HttpRequest,
    maybe_user_profile: Union[UserProfile, AnonymousUser],
    email_or_id: str,
    medium: bool = False,
) -> HttpResponse:
    """Accepts an email address or user ID and returns the avatar"""
    is_email = False
    try:
        int(email_or_id)
    except ValueError:
        is_email = True

    if not maybe_user_profile.is_authenticated:
        # Allow anonymous access to avatars only if spectators are
        # enabled in the organization.
        realm = get_valid_realm_from_request(request)
        if not realm.allow_web_public_streams_access():
            raise MissingAuthenticationError()

        # We only allow the ID format for accessing a user's avatar
        # for spectators. This is mainly for defense in depth, since
        # email_address_visibility should mean spectators only
        # interact with fake email addresses anyway.
        if is_email:
            raise MissingAuthenticationError()

        if settings.RATE_LIMITING:
            try:
                unique_avatar_key = f"{realm.id}/{email_or_id}/{medium}"
                rate_limit_spectator_attachment_access_by_file(
                    unique_avatar_key)
            except RateLimited:
                return json_response_from_error(
                    RateLimited(
                        _("Too many attempts, please try after some time.")))
    else:
        realm = maybe_user_profile.realm

    try:
        if is_email:
            avatar_user_profile = get_user_including_cross_realm(
                email_or_id, realm)
        else:
            avatar_user_profile = get_user_by_id_in_realm_including_cross_realm(
                int(email_or_id), realm)
        # If there is a valid user account passed in, use its avatar
        url = avatar_url(avatar_user_profile, medium=medium)
    except UserProfile.DoesNotExist:
        # If there is no such user, treat it as a new gravatar
        email = email_or_id
        avatar_version = 1
        url = get_gravatar_url(email, avatar_version, medium)

    # We can rely on the URL already having query parameters. Because
    # our templates depend on being able to use the ampersand to
    # add query parameters to our url, get_avatar_url does '?x=x'
    # hacks to prevent us from having to jump through decode/encode hoops.
    assert url is not None
    url = append_url_query_string(url, request.META["QUERY_STRING"])
    return redirect(url)
예제 #14
0
def rate_limit_password_reset_form_by_email(email: str) -> None:
    ratelimited, secs_to_freedom = RateLimitedPasswordResetByEmail(
        email).rate_limit()
    if ratelimited:
        raise RateLimited(secs_to_freedom)