mirror of
https://github.com/zulip/zulip.git
synced 2025-11-04 22:13:26 +00:00
push_notifications: Redact content for older clients if E2EE required.
This commit replaces the `PUSH_NOTIFICATION_REDACT_CONTENT` server setting with `require_e2ee_push_notifications` realm setting. If `require_e2ee_push_notifications` set to True: * Older clients: Content redacted * Updated clients: Encrypted content If `require_e2ee_push_notifications` set to False: * Older clients: Content NOT redacted * Updated clients: Encrypted content Note: Older clients refers to clients that don't support E2EE. Fixes part of #35370.
This commit is contained in:
committed by
Tim Abbott
parent
fc6cd9a966
commit
d972bb1ca9
@@ -971,9 +971,6 @@ def get_mobile_push_content(rendered_content: str) -> str:
|
|||||||
else:
|
else:
|
||||||
child.getparent().replace(child, collapse_element)
|
child.getparent().replace(child, collapse_element)
|
||||||
|
|
||||||
if settings.PUSH_NOTIFICATION_REDACT_CONTENT:
|
|
||||||
return _("New message")
|
|
||||||
|
|
||||||
elem = lxml.html.fragment_fromstring(rendered_content, create_parent=True)
|
elem = lxml.html.fragment_fromstring(rendered_content, create_parent=True)
|
||||||
change_katex_to_raw_latex(elem)
|
change_katex_to_raw_latex(elem)
|
||||||
potentially_collapse_quotes(elem)
|
potentially_collapse_quotes(elem)
|
||||||
@@ -1324,6 +1321,18 @@ def send_push_notifications_legacy(
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# While sending push notifications for new messages to older clients
|
||||||
|
# (which don't support E2EE), if `require_e2ee_push_notifications`
|
||||||
|
# realm setting is set to `true`, we redact the content.
|
||||||
|
if gcm_payload.get("event") != "remove" and user_profile.realm.require_e2ee_push_notifications:
|
||||||
|
# Make deep copies so redaction doesn't affect the original dicts
|
||||||
|
apns_payload = copy.deepcopy(apns_payload)
|
||||||
|
gcm_payload = copy.deepcopy(gcm_payload)
|
||||||
|
|
||||||
|
placeholder_content = _("New message")
|
||||||
|
apns_payload["alert"]["body"] = placeholder_content
|
||||||
|
gcm_payload["content"] = placeholder_content
|
||||||
|
|
||||||
if uses_notification_bouncer():
|
if uses_notification_bouncer():
|
||||||
send_notifications_to_bouncer(
|
send_notifications_to_bouncer(
|
||||||
user_profile, apns_payload, gcm_payload, gcm_options, android_devices, apple_devices
|
user_profile, apns_payload, gcm_payload, gcm_options, android_devices, apple_devices
|
||||||
|
|||||||
@@ -678,3 +678,83 @@ class RemovePushNotificationTest(E2EEPushNotificationTestCase):
|
|||||||
f"Sent E2EE mobile push notifications for user {hamlet.id}: 1 via FCM, 1 via APNs",
|
f"Sent E2EE mobile push notifications for user {hamlet.id}: 1 via FCM, 1 via APNs",
|
||||||
zerver_logger.output[2],
|
zerver_logger.output[2],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RequireE2EEPushNotificationsSettingTest(E2EEPushNotificationTestCase):
|
||||||
|
def test_content_redacted(self) -> None:
|
||||||
|
hamlet = self.example_user("hamlet")
|
||||||
|
aaron = self.example_user("aaron")
|
||||||
|
realm = hamlet.realm
|
||||||
|
|
||||||
|
self.register_old_push_devices_for_notification()
|
||||||
|
self.register_push_devices_for_notification()
|
||||||
|
|
||||||
|
message_id = self.send_personal_message(
|
||||||
|
from_user=aaron,
|
||||||
|
to_user=hamlet,
|
||||||
|
content="not-redacted",
|
||||||
|
skip_capture_on_commit_callbacks=True,
|
||||||
|
)
|
||||||
|
missed_message = {
|
||||||
|
"message_id": message_id,
|
||||||
|
"trigger": NotificationTriggers.DIRECT_MESSAGE,
|
||||||
|
}
|
||||||
|
|
||||||
|
realm.require_e2ee_push_notifications = True
|
||||||
|
realm.save(update_fields=["require_e2ee_push_notifications"])
|
||||||
|
|
||||||
|
# Verify that the content is redacted in payloads supplied to
|
||||||
|
# 'send_notifications_to_bouncer' - payloads supplied to bouncer (legacy codepath).
|
||||||
|
#
|
||||||
|
# Verify that the content is not redacted in payloads supplied to
|
||||||
|
# 'send_push_notifications' - payloads which get encrypted.
|
||||||
|
with (
|
||||||
|
activate_push_notification_service(),
|
||||||
|
mock.patch(
|
||||||
|
"zerver.lib.push_notifications.send_notifications_to_bouncer"
|
||||||
|
) as mock_legacy,
|
||||||
|
mock.patch("zerver.lib.push_notifications.send_push_notifications") as mock_e2ee,
|
||||||
|
):
|
||||||
|
handle_push_notification(hamlet.id, missed_message)
|
||||||
|
|
||||||
|
mock_legacy.assert_called_once()
|
||||||
|
self.assertEqual(mock_legacy.call_args.args[1]["alert"]["body"], "New message")
|
||||||
|
self.assertEqual(mock_legacy.call_args.args[2]["content"], "New message")
|
||||||
|
|
||||||
|
mock_e2ee.assert_called_once()
|
||||||
|
self.assertEqual(mock_e2ee.call_args.args[1]["alert"]["body"], "not-redacted")
|
||||||
|
self.assertEqual(mock_e2ee.call_args.args[2]["content"], "not-redacted")
|
||||||
|
|
||||||
|
message_id = self.send_personal_message(
|
||||||
|
from_user=aaron, to_user=hamlet, skip_capture_on_commit_callbacks=True
|
||||||
|
)
|
||||||
|
missed_message = {
|
||||||
|
"message_id": message_id,
|
||||||
|
"trigger": NotificationTriggers.DIRECT_MESSAGE,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify that the content is redacted in payloads supplied to
|
||||||
|
# to functions for sending it through APNs and FCM directly.
|
||||||
|
with (
|
||||||
|
mock.patch("zerver.lib.push_notifications.has_apns_credentials", return_value=True),
|
||||||
|
mock.patch("zerver.lib.push_notifications.has_fcm_credentials", return_value=True),
|
||||||
|
mock.patch(
|
||||||
|
"zerver.lib.push_notifications.send_notifications_to_bouncer"
|
||||||
|
) as send_bouncer,
|
||||||
|
mock.patch(
|
||||||
|
"zerver.lib.push_notifications.send_apple_push_notification", return_value=0
|
||||||
|
) as send_apple,
|
||||||
|
mock.patch(
|
||||||
|
"zerver.lib.push_notifications.send_android_push_notification", return_value=0
|
||||||
|
) as send_android,
|
||||||
|
# We have already asserted the payloads passed to E2EE codepath above.
|
||||||
|
mock.patch("zerver.lib.push_notifications.send_push_notifications"),
|
||||||
|
):
|
||||||
|
handle_push_notification(hamlet.id, missed_message)
|
||||||
|
|
||||||
|
send_bouncer.assert_not_called()
|
||||||
|
send_apple.assert_called_once()
|
||||||
|
send_android.assert_called_once()
|
||||||
|
|
||||||
|
self.assertEqual(send_apple.call_args.args[2]["alert"]["body"], "New message")
|
||||||
|
self.assertEqual(send_android.call_args.args[2]["content"], "New message")
|
||||||
|
|||||||
@@ -1836,49 +1836,6 @@ class TestGetAPNsPayload(PushNotificationTestCase):
|
|||||||
NotificationTriggers.STREAM_WILDCARD_MENTION
|
NotificationTriggers.STREAM_WILDCARD_MENTION
|
||||||
)
|
)
|
||||||
|
|
||||||
@override_settings(PUSH_NOTIFICATION_REDACT_CONTENT=True)
|
|
||||||
def test_get_message_payload_apns_redacted_content(self) -> None:
|
|
||||||
user_profile = self.example_user("othello")
|
|
||||||
message_id = self.send_group_direct_message(
|
|
||||||
self.sender, [self.example_user("othello"), self.example_user("cordelia")]
|
|
||||||
)
|
|
||||||
message = Message.objects.get(id=message_id)
|
|
||||||
payload = get_message_payload_apns(
|
|
||||||
user_profile, message, NotificationTriggers.DIRECT_MESSAGE
|
|
||||||
)
|
|
||||||
expected = {
|
|
||||||
"alert": {
|
|
||||||
"title": "Cordelia, Lear's daughter, King Hamlet, Othello, the Moor of Venice",
|
|
||||||
"subtitle": "King Hamlet:",
|
|
||||||
"body": "New message",
|
|
||||||
},
|
|
||||||
"sound": "default",
|
|
||||||
"badge": 0,
|
|
||||||
"custom": {
|
|
||||||
"zulip": {
|
|
||||||
"message_ids": [message.id],
|
|
||||||
"recipient_type": "private",
|
|
||||||
"pm_users": ",".join(
|
|
||||||
str(user_profile_id)
|
|
||||||
for user_profile_id in sorted(
|
|
||||||
s.user_profile_id
|
|
||||||
for s in Subscription.objects.filter(recipient=message.recipient)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"sender_email": self.sender.email,
|
|
||||||
"sender_id": self.sender.id,
|
|
||||||
"server": settings.EXTERNAL_HOST,
|
|
||||||
"realm_id": self.sender.realm.id,
|
|
||||||
"realm_name": self.sender.realm.name,
|
|
||||||
"realm_uri": self.sender.realm.url,
|
|
||||||
"realm_url": self.sender.realm.url,
|
|
||||||
"user_id": user_profile.id,
|
|
||||||
"time": datetime_to_timestamp(message.date_sent),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
self.assertDictEqual(payload, expected)
|
|
||||||
|
|
||||||
def test_get_message_payload_apns_stream_message_from_inaccessible_user(self) -> None:
|
def test_get_message_payload_apns_stream_message_from_inaccessible_user(self) -> None:
|
||||||
self.set_up_db_for_testing_user_access()
|
self.set_up_db_for_testing_user_access()
|
||||||
|
|
||||||
@@ -2048,43 +2005,6 @@ class TestGetGCMPayload(PushNotificationTestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@override_settings(PUSH_NOTIFICATION_REDACT_CONTENT=True)
|
|
||||||
def test_get_message_payload_gcm_redacted_content(self) -> None:
|
|
||||||
stream = Stream.objects.get(name="Denmark")
|
|
||||||
message = self.get_message(Recipient.STREAM, stream.id, stream.realm_id)
|
|
||||||
hamlet = self.example_user("hamlet")
|
|
||||||
payload, gcm_options = get_message_payload_gcm(hamlet, message)
|
|
||||||
self.assertDictEqual(
|
|
||||||
payload,
|
|
||||||
{
|
|
||||||
"user_id": hamlet.id,
|
|
||||||
"event": "message",
|
|
||||||
"zulip_message_id": message.id,
|
|
||||||
"time": datetime_to_timestamp(message.date_sent),
|
|
||||||
"content": "New message",
|
|
||||||
"content_truncated": False,
|
|
||||||
"server": settings.EXTERNAL_HOST,
|
|
||||||
"realm_id": hamlet.realm.id,
|
|
||||||
"realm_name": hamlet.realm.name,
|
|
||||||
"realm_uri": hamlet.realm.url,
|
|
||||||
"realm_url": hamlet.realm.url,
|
|
||||||
"sender_id": hamlet.id,
|
|
||||||
"sender_email": hamlet.email,
|
|
||||||
"sender_full_name": "King Hamlet",
|
|
||||||
"sender_avatar_url": absolute_avatar_url(message.sender),
|
|
||||||
"recipient_type": "stream",
|
|
||||||
"topic": "Test topic",
|
|
||||||
"stream": "Denmark",
|
|
||||||
"stream_id": stream.id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertDictEqual(
|
|
||||||
gcm_options,
|
|
||||||
{
|
|
||||||
"priority": "high",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_get_message_payload_gcm_stream_message_from_inaccessible_user(self) -> None:
|
def test_get_message_payload_gcm_stream_message_from_inaccessible_user(self) -> None:
|
||||||
self.set_up_db_for_testing_user_access()
|
self.set_up_db_for_testing_user_access()
|
||||||
|
|
||||||
|
|||||||
@@ -243,8 +243,6 @@ ZULIP_SERVICE_PUSH_NOTIFICATIONS = False
|
|||||||
ZULIP_SERVICE_SUBMIT_USAGE_STATISTICS: bool | None = None
|
ZULIP_SERVICE_SUBMIT_USAGE_STATISTICS: bool | None = None
|
||||||
ZULIP_SERVICE_SECURITY_ALERTS = False
|
ZULIP_SERVICE_SECURITY_ALERTS = False
|
||||||
|
|
||||||
PUSH_NOTIFICATION_REDACT_CONTENT = False
|
|
||||||
|
|
||||||
# Old setting kept around for backwards compatibility. Some old servers
|
# Old setting kept around for backwards compatibility. Some old servers
|
||||||
# may have it in their settings.py.
|
# may have it in their settings.py.
|
||||||
PUSH_NOTIFICATION_BOUNCER_URL: str | None = None
|
PUSH_NOTIFICATION_BOUNCER_URL: str | None = None
|
||||||
|
|||||||
@@ -779,12 +779,6 @@ SOCIAL_AUTH_SAML_SUPPORT_CONTACT = {
|
|||||||
## can disable submitting usage statistics here.
|
## can disable submitting usage statistics here.
|
||||||
# ZULIP_SERVICE_SUBMIT_USAGE_STATISTICS = False
|
# ZULIP_SERVICE_SUBMIT_USAGE_STATISTICS = False
|
||||||
|
|
||||||
## Whether to redact the content of push notifications. This is less
|
|
||||||
## usable, but avoids sending message content over the wire. In the
|
|
||||||
## future, we're likely to replace this with an end-to-end push
|
|
||||||
## notification encryption feature.
|
|
||||||
# PUSH_NOTIFICATION_REDACT_CONTENT = False
|
|
||||||
|
|
||||||
## Whether to lightly advertise sponsoring Zulip in the gear menu.
|
## Whether to lightly advertise sponsoring Zulip in the gear menu.
|
||||||
# PROMOTE_SPONSORING_ZULIP = True
|
# PROMOTE_SPONSORING_ZULIP = True
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user