poll widget: Add server validation.

This commit is contained in:
Steve Howell
2021-06-13 15:00:45 +00:00
committed by Tim Abbott
parent ab9c17ed3d
commit e739bee00a
4 changed files with 207 additions and 4 deletions

View File

@@ -456,6 +456,49 @@ def check_widget_content(widget_content: object) -> Dict[str, Any]:
raise ValidationError("unknown widget type: " + widget_type) raise ValidationError("unknown widget type: " + widget_type)
def validate_poll_data(poll_data: object, is_widget_author: bool) -> None:
check_dict([("type", check_string)])("poll data", poll_data)
assert isinstance(poll_data, dict)
if poll_data["type"] == "vote":
checker = check_dict_only(
[
("type", check_string),
("key", check_string),
("vote", check_int_in([1, -1])),
]
)
checker("poll data", poll_data)
return
if poll_data["type"] == "question":
if not is_widget_author:
raise ValidationError("You can't edit a question unless you are the author.")
checker = check_dict_only(
[
("type", check_string),
("question", check_string),
]
)
checker("poll data", poll_data)
return
if poll_data["type"] == "new_option":
checker = check_dict_only(
[
("type", check_string),
("option", check_string),
("idx", check_int),
]
)
checker("poll data", poll_data)
return
raise ValidationError(f"Unknown type for poll data: {poll_data['type']}")
# Converter functions for use with has_request_variables # Converter functions for use with has_request_variables
def to_non_negative_int(s: str, max_int_size: int = 2 ** 32 - 1) -> int: def to_non_negative_int(s: str, max_int_size: int = 2 ** 32 - 1) -> int:
x = int(s) x = int(s)

View File

@@ -79,6 +79,30 @@ def do_widget_post_save_actions(send_request: SendMessageRequest) -> None:
send_request.submessages = SubMessage.get_raw_db_rows([message_id]) send_request.submessages = SubMessage.get_raw_db_rows([message_id])
def get_widget_type(*, message_id: int) -> Optional[str]:
submessage = (
SubMessage.objects.filter(
message_id=message_id,
msg_type="widget",
)
.only("content")
.first()
)
if submessage is None:
return None
try:
data = json.loads(submessage.content)
except Exception:
return None
try:
return data["widget_type"]
except Exception:
return None
def is_widget_message(message: Message) -> bool: def is_widget_message(message: Message) -> bool:
# Right now all messages that are widgetized use submessage, and vice versa. # Right now all messages that are widgetized use submessage, and vice versa.
return message.submessage_set.exists() return message.submessage_set.exists()

View File

@@ -3,11 +3,12 @@ from typing import Any, Dict
import orjson import orjson
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.http import HttpResponse
from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.validator import check_widget_content from zerver.lib.validator import check_widget_content
from zerver.lib.widget import get_widget_data from zerver.lib.widget import get_widget_data, get_widget_type
from zerver.models import SubMessage from zerver.models import SubMessage, UserProfile
class WidgetContentTestCase(ZulipTestCase): class WidgetContentTestCase(ZulipTestCase):
@@ -223,3 +224,125 @@ class WidgetContentTestCase(ZulipTestCase):
submessage = SubMessage.objects.get(message_id=message.id) submessage = SubMessage.objects.get(message_id=message.id)
self.assertEqual(submessage.msg_type, "widget") self.assertEqual(submessage.msg_type, "widget")
self.assertEqual(orjson.loads(submessage.content), expected_submessage_content) self.assertEqual(orjson.loads(submessage.content), expected_submessage_content)
def test_poll_permissions(self) -> None:
cordelia = self.example_user("cordelia")
hamlet = self.example_user("hamlet")
stream_name = "Verona"
content = "/poll Preference?\n\nyes\nno"
payload = dict(
type="stream",
to=stream_name,
client="test suite",
topic="whatever",
content=content,
)
result = self.api_post(cordelia, "/api/v1/messages", payload)
self.assert_json_success(result)
message = self.get_last_message()
def post(sender: UserProfile, data: Dict[str, object]) -> HttpResponse:
payload = dict(
message_id=message.id, msg_type="widget", content=orjson.dumps(data).decode()
)
return self.api_post(sender, "/api/v1/submessage", payload)
result = post(cordelia, dict(type="question", question="Tabs or spaces?"))
self.assert_json_success(result)
result = post(hamlet, dict(type="question", question="Tabs or spaces?"))
self.assert_json_error(result, "You can't edit a question unless you are the author.")
def test_poll_type_validation(self) -> None:
sender = self.example_user("cordelia")
stream_name = "Verona"
content = "/poll Preference?\n\nyes\nno"
payload = dict(
type="stream",
to=stream_name,
client="test suite",
topic="whatever",
content=content,
)
result = self.api_post(sender, "/api/v1/messages", payload)
self.assert_json_success(result)
message = self.get_last_message()
def post_submessage(content: str) -> HttpResponse:
payload = dict(
message_id=message.id,
msg_type="widget",
content=content,
)
return self.api_post(sender, "/api/v1/submessage", payload)
def assert_error(content: str, error: str) -> None:
result = post_submessage(content)
self.assert_json_error_contains(result, error)
assert_error("bogus", "Invalid json for submessage")
assert_error('""', "not a dict")
assert_error("[]", "not a dict")
assert_error('{"type": "bogus"}', "Unknown type for poll data: bogus")
assert_error('{"type": "vote"}', "key is missing")
assert_error('{"type": "vote", "key": "1,1,", "vote": 99}', "Invalid poll data")
assert_error('{"type": "question"}', "key is missing")
assert_error('{"type": "question", "question": 7}', "not a string")
assert_error('{"type": "new_option"}', "key is missing")
assert_error('{"type": "new_option", "idx": 7, "option": 999}', "not a string")
assert_error('{"type": "new_option", "idx": "bogus", "option": "maybe"}', "not an int")
def assert_success(data: Dict[str, object]) -> None:
content = orjson.dumps(data).decode()
result = post_submessage(content)
self.assert_json_success(result)
# Note that we only validate for types. The server code may, for,
# example, allow a vote for a non-existing option, and we rely
# on the clients to ignore those.
assert_success(dict(type="vote", key="1,1", vote=1))
assert_success(dict(type="new_option", idx=7, option="maybe"))
assert_success(dict(type="question", question="what's for dinner?"))
def test_get_widget_type(self) -> None:
sender = self.example_user("cordelia")
stream_name = "Verona"
# We test for both trailing and leading spaces, along with blank lines
# for the poll options.
content = "/poll Preference?\n\nyes\nno"
payload = dict(
type="stream",
to=stream_name,
client="test suite",
topic="whatever",
content=content,
)
result = self.api_post(sender, "/api/v1/messages", payload)
self.assert_json_success(result)
message = self.get_last_message()
[submessage] = SubMessage.objects.filter(message_id=message.id)
self.assertEqual(get_widget_type(message_id=message.id), "poll")
submessage.content = "bogus non json"
submessage.save()
self.assertEqual(get_widget_type(message_id=message.id), None)
submessage.content = '{"bogus": 1}'
submessage.save()
self.assertEqual(get_widget_type(message_id=message.id), None)
submessage.content = '{"widget_type": "todo"}'
submessage.save()
self.assertEqual(get_widget_type(message_id=message.id), "todo")

View File

@@ -1,13 +1,16 @@
import orjson import orjson
from django.core.exceptions import ValidationError
from django.db import transaction from django.db import transaction
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from zerver.decorator import REQ, has_request_variables from zerver.decorator import REQ, has_request_variables
from zerver.lib.actions import do_add_submessage, verify_submessage_sender from zerver.lib.actions import do_add_submessage, verify_submessage_sender
from zerver.lib.exceptions import JsonableError
from zerver.lib.message import access_message from zerver.lib.message import access_message
from zerver.lib.response import json_error, json_success from zerver.lib.response import json_error, json_success
from zerver.lib.validator import check_int from zerver.lib.validator import check_int, validate_poll_data
from zerver.lib.widget import get_widget_type
from zerver.models import UserProfile from zerver.models import UserProfile
@@ -30,10 +33,20 @@ def process_submessage(
) )
try: try:
orjson.loads(content) widget_data = orjson.loads(content)
except orjson.JSONDecodeError: except orjson.JSONDecodeError:
return json_error(_("Invalid json for submessage")) return json_error(_("Invalid json for submessage"))
widget_type = get_widget_type(message_id=message.id)
is_widget_author = message.sender_id == user_profile.id
if widget_type == "poll":
try:
validate_poll_data(poll_data=widget_data, is_widget_author=is_widget_author)
except ValidationError as error:
raise JsonableError(error.message)
do_add_submessage( do_add_submessage(
realm=user_profile.realm, realm=user_profile.realm,
sender_id=user_profile.id, sender_id=user_profile.id,