message_edit: Allow spectators to access raw message content.

We allow spectators to fetch the raw / original content of a
message which is used by the spectator to "View source" of
the message.
This commit is contained in:
Aman Agrawal 2021-09-20 14:04:10 +05:30 committed by Tim Abbott
parent e556481ba0
commit ef84224eed
4 changed files with 131 additions and 6 deletions

View File

@ -24,7 +24,7 @@ from zerver.lib.cache import (
to_dict_cache_key_id,
)
from zerver.lib.display_recipient import bulk_fetch_display_recipients
from zerver.lib.exceptions import JsonableError
from zerver.lib.exceptions import JsonableError, MissingAuthenticationError
from zerver.lib.markdown import MessageRenderingResult, markdown_convert, topic_links
from zerver.lib.markdown import version as markdown_version
from zerver.lib.mention import MentionData
@ -33,6 +33,7 @@ from zerver.lib.stream_subscription import (
get_subscribed_stream_recipient_ids_for_user,
num_subscribers_for_stream_id,
)
from zerver.lib.streams import get_web_public_streams_queryset
from zerver.lib.timestamp import datetime_to_timestamp
from zerver.lib.topic import DB_TOPIC_NAME, MESSAGE__TOPIC, TOPIC_LINKS, TOPIC_NAME
from zerver.lib.topic_mutes import build_topic_mute_checker, topic_is_muted
@ -709,6 +710,48 @@ def access_message(
raise JsonableError(_("Invalid message(s)"))
def access_web_public_message(
realm: Realm,
message_id: int,
) -> Message:
"""Access control method for unauthenticated requests interacting
with a message in web public streams.
"""
# We throw a MissingAuthenticationError for all errors in this
# code path, to avoid potentially leaking information on whether a
# message with the provided ID exists on the server if the client
# shouldn't have access to it.
if not realm.web_public_streams_enabled():
raise MissingAuthenticationError()
try:
message = Message.objects.select_related().get(id=message_id)
except Message.DoesNotExist:
raise MissingAuthenticationError()
if not message.is_stream_message():
raise MissingAuthenticationError()
queryset = get_web_public_streams_queryset(realm)
try:
stream = queryset.get(id=message.recipient.type_id)
except Stream.DoesNotExist:
raise MissingAuthenticationError()
# These should all have been enforced by the code in
# get_web_public_streams_queryset
assert stream.is_web_public
assert not stream.deactivated
assert not stream.invite_only
assert stream.history_public_to_subscribers
# Now that we've confirmed this message was sent to the target
# web-public stream, we can return it as having been successfully
# accessed.
return message
def has_message_access(
user_profile: UserProfile,
message: Message,

View File

@ -9,8 +9,10 @@ from django.http import HttpResponse
from django.utils.timezone import now as timezone_now
from zerver.lib.actions import (
do_change_plan_type,
do_change_stream_post_policy,
do_change_user_role,
do_deactivate_stream,
do_delete_messages,
do_set_realm_property,
do_update_message,
@ -311,6 +313,78 @@ class EditMessageTest(EditMessageTestCase):
result = self.client_get("/json/messages/" + str(msg_id))
self.assert_json_error(result, "Invalid message(s)")
def test_fetch_raw_message_spectator(self) -> None:
user_profile = self.example_user("iago")
self.login("iago")
web_public_stream = self.make_stream("web-public-stream", is_web_public=True)
self.subscribe(user_profile, web_public_stream.name)
web_public_stream_msg_id = self.send_stream_message(
user_profile, web_public_stream.name, content="web-public message"
)
non_web_public_stream = self.make_stream("non-web-public-stream")
non_web_public_stream_msg_id = self.send_stream_message(
user_profile, non_web_public_stream.name, content="non web-public message"
)
# Generate a private message to use in verification.
private_message_id = self.send_personal_message(user_profile, user_profile)
invalid_message_id = private_message_id + 1000
self.logout()
# Confirm WEB_PUBLIC_STREAMS_ENABLED is enforced.
with self.settings(WEB_PUBLIC_STREAMS_ENABLED=False):
result = self.client_get("/json/messages/" + str(web_public_stream_msg_id))
self.assert_json_error(
result, "Not logged in: API authentication or user session required", 401
)
# Verify success with web-public stream and default SELF_HOSTED plan type.
result = self.client_get("/json/messages/" + str(web_public_stream_msg_id))
self.assert_json_success(result)
self.assertEqual(result.json()["raw_content"], "web-public message")
# Verify LIMITED plan type does not allow web-public access.
do_change_plan_type(user_profile.realm, Realm.LIMITED, acting_user=None)
result = self.client_get("/json/messages/" + str(web_public_stream_msg_id))
self.assert_json_error(
result, "Not logged in: API authentication or user session required", 401
)
# Verify works with STANDARD_FREE plan type too.
do_change_plan_type(user_profile.realm, Realm.STANDARD_FREE, acting_user=None)
result = self.client_get("/json/messages/" + str(web_public_stream_msg_id))
self.assert_json_success(result)
self.assertEqual(result.json()["raw_content"], "web-public message")
# Verify private messages are rejected.
result = self.client_get("/json/messages/" + str(private_message_id))
self.assert_json_error(
result, "Not logged in: API authentication or user session required", 401
)
# Verify an actual public stream is required.
result = self.client_get("/json/messages/" + str(non_web_public_stream_msg_id))
self.assert_json_error(
result, "Not logged in: API authentication or user session required", 401
)
# Verify invalid message IDs are rejected with the same error message.
result = self.client_get("/json/messages/" + str(invalid_message_id))
self.assert_json_error(
result, "Not logged in: API authentication or user session required", 401
)
# Verify deactivated streams are rejected. This may change in the future.
do_deactivate_stream(web_public_stream, acting_user=None)
result = self.client_get("/json/messages/" + str(web_public_stream_msg_id))
self.assert_json_error(
result, "Not logged in: API authentication or user session required", 401
)
def test_fetch_raw_message_stream_wrong_realm(self) -> None:
user_profile = self.example_user("hamlet")
self.login_user(user_profile)

View File

@ -1,16 +1,18 @@
import datetime
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Union
import orjson
from django.contrib.auth.models import AnonymousUser
from django.db import IntegrityError, transaction
from django.http import HttpRequest, HttpResponse
from django.utils.timezone import now as timezone_now
from django.utils.translation import gettext as _
from zerver.context_processors import get_valid_realm_from_request
from zerver.lib.actions import check_update_message, do_delete_messages
from zerver.lib.exceptions import JsonableError
from zerver.lib.html_diff import highlight_html_differences
from zerver.lib.message import access_message
from zerver.lib.message import access_message, access_web_public_message
from zerver.lib.request import REQ, RequestNotes, has_request_variables
from zerver.lib.response import json_success
from zerver.lib.timestamp import datetime_to_timestamp
@ -168,8 +170,14 @@ def delete_message_backend(
@has_request_variables
def json_fetch_raw_message(
request: HttpRequest,
user_profile: UserProfile,
maybe_user_profile: Union[UserProfile, AnonymousUser],
message_id: int = REQ(converter=to_non_negative_int, path_only=True),
) -> HttpResponse:
(message, user_message) = access_message(user_profile, message_id)
if not maybe_user_profile.is_authenticated:
realm = get_valid_realm_from_request(request)
message = access_web_public_message(realm, message_id)
else:
(message, user_message) = access_message(maybe_user_profile, message_id)
return json_success({"raw_content": message.content})

View File

@ -324,7 +324,7 @@ v1_api_and_json_patterns = [
),
rest_path(
"messages/<int:message_id>",
GET=json_fetch_raw_message,
GET=(json_fetch_raw_message, {"allow_anonymous_user_web"}),
PATCH=update_message_backend,
DELETE=delete_message_backend,
),