mirror of https://github.com/zulip/zulip.git
users: Update presence and user status code to support restricted users.
The presence and user status update events are only sent to accessible users, i.e. guests do not receive presence and user status updates for users they cannot access.
This commit is contained in:
parent
650e55fef8
commit
dbcc9ea826
|
@ -25,6 +25,19 @@ format used by the Zulip server that they are interacting with.
|
||||||
* [`GET /events`](/api/get-events): `realm_user` events with `op: "update"`
|
* [`GET /events`](/api/get-events): `realm_user` events with `op: "update"`
|
||||||
are now only sent to users who can access the modified user.
|
are now only sent to users who can access the modified user.
|
||||||
|
|
||||||
|
* [`GET /events`](/api/get-events): `presence` events are now only sent to
|
||||||
|
users who can access the user who comes back online if the
|
||||||
|
`CAN_ACCESS_ALL_USERS_GROUP_LIMITS_PRESENCE` server setting is set
|
||||||
|
to `true`.
|
||||||
|
|
||||||
|
* [`GET /events`](/api/get-events): `user_status` events are now only
|
||||||
|
sent to users who can access the modified user.
|
||||||
|
|
||||||
|
* [`GET /realm/presence`](/api/get-presence): The endpoint now returns
|
||||||
|
presence information of accessible users only if the
|
||||||
|
`CAN_ACCESS_ALL_USERS_GROUP_LIMITS_PRESENCE` server setting is set
|
||||||
|
to `true`.
|
||||||
|
|
||||||
**Feature level 227**
|
**Feature level 227**
|
||||||
|
|
||||||
* [`PATCH /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults),
|
* [`PATCH /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults),
|
||||||
|
|
|
@ -11,6 +11,7 @@ from zerver.lib.presence import (
|
||||||
)
|
)
|
||||||
from zerver.lib.queue import queue_json_publish
|
from zerver.lib.queue import queue_json_publish
|
||||||
from zerver.lib.timestamp import datetime_to_timestamp
|
from zerver.lib.timestamp import datetime_to_timestamp
|
||||||
|
from zerver.lib.users import get_user_ids_who_can_access_user
|
||||||
from zerver.models import Client, UserPresence, UserProfile, active_user_ids, get_client
|
from zerver.models import Client, UserPresence, UserProfile, active_user_ids, get_client
|
||||||
from zerver.tornado.django_api import send_event
|
from zerver.tornado.django_api import send_event
|
||||||
|
|
||||||
|
@ -27,7 +28,11 @@ def send_presence_changed(
|
||||||
#
|
#
|
||||||
# See https://zulip.readthedocs.io/en/latest/subsystems/presence.html for
|
# See https://zulip.readthedocs.io/en/latest/subsystems/presence.html for
|
||||||
# internals documentation on presence.
|
# internals documentation on presence.
|
||||||
user_ids = active_user_ids(user_profile.realm_id)
|
if settings.CAN_ACCESS_ALL_USERS_GROUP_LIMITS_PRESENCE:
|
||||||
|
user_ids = get_user_ids_who_can_access_user(user_profile)
|
||||||
|
else:
|
||||||
|
user_ids = active_user_ids(user_profile.realm_id)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
len(user_ids) > settings.USER_LIMIT_FOR_SENDING_PRESENCE_UPDATE_EVENTS
|
len(user_ids) > settings.USER_LIMIT_FOR_SENDING_PRESENCE_UPDATE_EVENTS
|
||||||
and not force_send_update
|
and not force_send_update
|
||||||
|
|
|
@ -2,7 +2,8 @@ from typing import Optional
|
||||||
|
|
||||||
from zerver.actions.user_settings import do_change_user_setting
|
from zerver.actions.user_settings import do_change_user_setting
|
||||||
from zerver.lib.user_status import update_user_status
|
from zerver.lib.user_status import update_user_status
|
||||||
from zerver.models import UserProfile, active_user_ids
|
from zerver.lib.users import get_user_ids_who_can_access_user
|
||||||
|
from zerver.models import UserProfile
|
||||||
from zerver.tornado.django_api import send_event
|
from zerver.tornado.django_api import send_event
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,4 +51,4 @@ def do_update_user_status(
|
||||||
event["emoji_name"] = emoji_name
|
event["emoji_name"] = emoji_name
|
||||||
event["emoji_code"] = emoji_code
|
event["emoji_code"] = emoji_code
|
||||||
event["reaction_type"] = reaction_type
|
event["reaction_type"] = reaction_type
|
||||||
send_event(realm, event, active_user_ids(realm.id))
|
send_event(realm, event, get_user_ids_who_can_access_user(user_profile))
|
||||||
|
|
|
@ -231,7 +231,9 @@ def fetch_initial_state_data(
|
||||||
|
|
||||||
if want("presence"):
|
if want("presence"):
|
||||||
state["presences"] = (
|
state["presences"] = (
|
||||||
{} if user_profile is None else get_presences_for_realm(realm, slim_presence)
|
{}
|
||||||
|
if user_profile is None
|
||||||
|
else get_presences_for_realm(realm, slim_presence, user_profile)
|
||||||
)
|
)
|
||||||
# Send server_timestamp, to match the format of `GET /presence` requests.
|
# Send server_timestamp, to match the format of `GET /presence` requests.
|
||||||
state["server_timestamp"] = time.time()
|
state["server_timestamp"] = time.time()
|
||||||
|
@ -652,7 +654,9 @@ def fetch_initial_state_data(
|
||||||
if want("user_status"):
|
if want("user_status"):
|
||||||
# We require creating an account to access statuses.
|
# We require creating an account to access statuses.
|
||||||
state["user_status"] = (
|
state["user_status"] = (
|
||||||
{} if user_profile is None else get_user_status_dict(realm_id=realm.id)
|
{}
|
||||||
|
if user_profile is None
|
||||||
|
else get_user_status_dict(realm=realm, user_profile=user_profile)
|
||||||
)
|
)
|
||||||
|
|
||||||
if want("user_topic"):
|
if want("user_topic"):
|
||||||
|
|
|
@ -7,6 +7,7 @@ from django.conf import settings
|
||||||
from django.utils.timezone import now as timezone_now
|
from django.utils.timezone import now as timezone_now
|
||||||
|
|
||||||
from zerver.lib.timestamp import datetime_to_timestamp
|
from zerver.lib.timestamp import datetime_to_timestamp
|
||||||
|
from zerver.lib.users import check_user_can_access_all_users, get_accessible_user_ids
|
||||||
from zerver.models import PushDeviceToken, Realm, UserPresence, UserProfile, query_for_ids
|
from zerver.models import PushDeviceToken, Realm, UserPresence, UserProfile, query_for_ids
|
||||||
|
|
||||||
|
|
||||||
|
@ -151,24 +152,33 @@ def get_presence_for_user(
|
||||||
|
|
||||||
|
|
||||||
def get_presence_dict_by_realm(
|
def get_presence_dict_by_realm(
|
||||||
realm_id: int, slim_presence: bool = False
|
realm: Realm, slim_presence: bool = False, requesting_user_profile: Optional[UserProfile] = None
|
||||||
) -> Dict[str, Dict[str, Any]]:
|
) -> Dict[str, Dict[str, Any]]:
|
||||||
two_weeks_ago = timezone_now() - datetime.timedelta(weeks=2)
|
two_weeks_ago = timezone_now() - datetime.timedelta(weeks=2)
|
||||||
query = UserPresence.objects.filter(
|
query = UserPresence.objects.filter(
|
||||||
realm_id=realm_id,
|
realm_id=realm.id,
|
||||||
last_connected_time__gte=two_weeks_ago,
|
last_connected_time__gte=two_weeks_ago,
|
||||||
user_profile__is_active=True,
|
user_profile__is_active=True,
|
||||||
user_profile__is_bot=False,
|
user_profile__is_bot=False,
|
||||||
).values(
|
|
||||||
"last_active_time",
|
|
||||||
"last_connected_time",
|
|
||||||
"user_profile__email",
|
|
||||||
"user_profile_id",
|
|
||||||
"user_profile__enable_offline_push_notifications",
|
|
||||||
"user_profile__date_joined",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
presence_rows = list(query)
|
if settings.CAN_ACCESS_ALL_USERS_GROUP_LIMITS_PRESENCE and not check_user_can_access_all_users(
|
||||||
|
requesting_user_profile
|
||||||
|
):
|
||||||
|
assert requesting_user_profile is not None
|
||||||
|
accessible_user_ids = get_accessible_user_ids(realm, requesting_user_profile)
|
||||||
|
query = query.filter(user_profile_id__in=accessible_user_ids)
|
||||||
|
|
||||||
|
presence_rows = list(
|
||||||
|
query.values(
|
||||||
|
"last_active_time",
|
||||||
|
"last_connected_time",
|
||||||
|
"user_profile__email",
|
||||||
|
"user_profile_id",
|
||||||
|
"user_profile__enable_offline_push_notifications",
|
||||||
|
"user_profile__date_joined",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
mobile_query = PushDeviceToken.objects.distinct("user_id").values_list(
|
mobile_query = PushDeviceToken.objects.distinct("user_id").values_list(
|
||||||
"user_id",
|
"user_id",
|
||||||
|
@ -196,13 +206,13 @@ def get_presence_dict_by_realm(
|
||||||
|
|
||||||
|
|
||||||
def get_presences_for_realm(
|
def get_presences_for_realm(
|
||||||
realm: Realm, slim_presence: bool
|
realm: Realm, slim_presence: bool, requesting_user_profile: UserProfile
|
||||||
) -> Dict[str, Dict[str, Dict[str, Any]]]:
|
) -> Dict[str, Dict[str, Dict[str, Any]]]:
|
||||||
if realm.presence_disabled:
|
if realm.presence_disabled:
|
||||||
# Return an empty dict if presence is disabled in this realm
|
# Return an empty dict if presence is disabled in this realm
|
||||||
return defaultdict(dict)
|
return defaultdict(dict)
|
||||||
|
|
||||||
return get_presence_dict_by_realm(realm.id, slim_presence)
|
return get_presence_dict_by_realm(realm, slim_presence, requesting_user_profile)
|
||||||
|
|
||||||
|
|
||||||
def get_presence_response(
|
def get_presence_response(
|
||||||
|
@ -210,5 +220,5 @@ def get_presence_response(
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
realm = requesting_user_profile.realm
|
realm = requesting_user_profile.realm
|
||||||
server_timestamp = time.time()
|
server_timestamp = time.time()
|
||||||
presences = get_presences_for_realm(realm, slim_presence)
|
presences = get_presences_for_realm(realm, slim_presence, requesting_user_profile)
|
||||||
return dict(presences=presences, server_timestamp=server_timestamp)
|
return dict(presences=presences, server_timestamp=server_timestamp)
|
||||||
|
|
|
@ -3,7 +3,8 @@ from typing import Dict, Optional, TypedDict
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.timezone import now as timezone_now
|
from django.utils.timezone import now as timezone_now
|
||||||
|
|
||||||
from zerver.models import UserStatus
|
from zerver.lib.users import check_user_can_access_all_users, get_accessible_user_ids
|
||||||
|
from zerver.models import Realm, UserProfile, UserStatus
|
||||||
|
|
||||||
|
|
||||||
class UserInfoDict(TypedDict, total=False):
|
class UserInfoDict(TypedDict, total=False):
|
||||||
|
@ -48,27 +49,29 @@ def format_user_status(row: RawUserInfoDict) -> UserInfoDict:
|
||||||
return dct
|
return dct
|
||||||
|
|
||||||
|
|
||||||
def get_user_status_dict(realm_id: int) -> Dict[str, UserInfoDict]:
|
def get_user_status_dict(realm: Realm, user_profile: UserProfile) -> Dict[str, UserInfoDict]:
|
||||||
rows = (
|
query = UserStatus.objects.filter(
|
||||||
UserStatus.objects.filter(
|
user_profile__realm_id=realm.id,
|
||||||
user_profile__realm_id=realm_id,
|
user_profile__is_active=True,
|
||||||
user_profile__is_active=True,
|
).exclude(
|
||||||
)
|
Q(user_profile__presence_enabled=True)
|
||||||
.exclude(
|
& Q(status_text="")
|
||||||
Q(user_profile__presence_enabled=True)
|
& Q(emoji_name="")
|
||||||
& Q(status_text="")
|
& Q(emoji_code="")
|
||||||
& Q(emoji_name="")
|
& Q(reaction_type=UserStatus.UNICODE_EMOJI),
|
||||||
& Q(emoji_code="")
|
)
|
||||||
& Q(reaction_type=UserStatus.UNICODE_EMOJI),
|
|
||||||
)
|
if not check_user_can_access_all_users(user_profile):
|
||||||
.values(
|
accessible_user_ids = get_accessible_user_ids(realm, user_profile)
|
||||||
"user_profile_id",
|
query = query.filter(user_profile_id__in=accessible_user_ids)
|
||||||
"user_profile__presence_enabled",
|
|
||||||
"status_text",
|
rows = query.values(
|
||||||
"emoji_name",
|
"user_profile_id",
|
||||||
"emoji_code",
|
"user_profile__presence_enabled",
|
||||||
"reaction_type",
|
"status_text",
|
||||||
)
|
"emoji_name",
|
||||||
|
"emoji_code",
|
||||||
|
"reaction_type",
|
||||||
)
|
)
|
||||||
|
|
||||||
user_dict: Dict[str, UserInfoDict] = {}
|
user_dict: Dict[str, UserInfoDict] = {}
|
||||||
|
|
|
@ -591,7 +591,8 @@ def check_can_access_user(
|
||||||
|
|
||||||
def get_user_ids_who_can_access_user(target_user: UserProfile) -> List[int]:
|
def get_user_ids_who_can_access_user(target_user: UserProfile) -> List[int]:
|
||||||
# We assume that caller only needs active users here, since
|
# We assume that caller only needs active users here, since
|
||||||
# this function is used to get users to send events.
|
# this function is used to get users to send events and to
|
||||||
|
# send presence update.
|
||||||
realm = target_user.realm
|
realm = target_user.realm
|
||||||
if not user_access_restricted_in_realm(target_user):
|
if not user_access_restricted_in_realm(target_user):
|
||||||
return active_user_ids(realm.id)
|
return active_user_ids(realm.id)
|
||||||
|
|
|
@ -1133,6 +1133,13 @@ paths:
|
||||||
confusing users when someone comes online and immediately sends
|
confusing users when someone comes online and immediately sends
|
||||||
a message (one wouldn't want them to still appear offline at
|
a message (one wouldn't want them to still appear offline at
|
||||||
that point!).
|
that point!).
|
||||||
|
|
||||||
|
If the `CAN_ACCESS_ALL_USERS_GROUP_LIMITS_PRESENCE` server-level
|
||||||
|
setting is set to `true`, then the event is only sent to users
|
||||||
|
who can access the user who came back online.
|
||||||
|
|
||||||
|
**Changes**: Prior to Zulip 8.0 (feature level 228), this event
|
||||||
|
was sent to all users in the organization.
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
$ref: "#/components/schemas/EventIdSchema"
|
$ref: "#/components/schemas/EventIdSchema"
|
||||||
|
@ -1702,8 +1709,11 @@ paths:
|
||||||
- type: object
|
- type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
description: |
|
description: |
|
||||||
Event sent to all users in a Zulip organization when the
|
Event sent to all users who can access the modified
|
||||||
status of a user changes.
|
user when the status of a user changes.
|
||||||
|
|
||||||
|
**Changes**: Prior to Zulip 8.0 (feature level 228),
|
||||||
|
this event was sent to all users in the organization.
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
$ref: "#/components/schemas/EventIdSchema"
|
$ref: "#/components/schemas/EventIdSchema"
|
||||||
|
@ -9761,6 +9771,10 @@ paths:
|
||||||
description: |
|
description: |
|
||||||
Get the presence information of all the users in an organization.
|
Get the presence information of all the users in an organization.
|
||||||
|
|
||||||
|
If the `CAN_ACCESS_ALL_USERS_GROUP_LIMITS_PRESENCE` server-level
|
||||||
|
setting is set to `true`, presence information of only accessible
|
||||||
|
users are returned.
|
||||||
|
|
||||||
See [Zulip's developer documentation][subsystems-presence]
|
See [Zulip's developer documentation][subsystems-presence]
|
||||||
for details on the data model for presence in Zulip.
|
for details on the data model for presence in Zulip.
|
||||||
|
|
||||||
|
|
|
@ -1645,6 +1645,55 @@ class NormalActionsTest(BaseAction):
|
||||||
|
|
||||||
check_user_status("events[0]", events[0], {"status_text"})
|
check_user_status("events[0]", events[0], {"status_text"})
|
||||||
|
|
||||||
|
self.set_up_db_for_testing_user_access()
|
||||||
|
cordelia = self.example_user("cordelia")
|
||||||
|
self.user_profile = self.example_user("polonius")
|
||||||
|
|
||||||
|
# Set the date_joined for cordelia here like we did at
|
||||||
|
# the start of this test.
|
||||||
|
cordelia.date_joined = timezone_now() - datetime.timedelta(days=15)
|
||||||
|
cordelia.save()
|
||||||
|
|
||||||
|
away_val = False
|
||||||
|
with self.settings(CAN_ACCESS_ALL_USERS_GROUP_LIMITS_PRESENCE=True):
|
||||||
|
events = self.verify_action(
|
||||||
|
lambda: do_update_user_status(
|
||||||
|
user_profile=cordelia,
|
||||||
|
away=away_val,
|
||||||
|
status_text="out to lunch",
|
||||||
|
emoji_name="car",
|
||||||
|
emoji_code="1f697",
|
||||||
|
reaction_type=UserStatus.UNICODE_EMOJI,
|
||||||
|
client_id=client.id,
|
||||||
|
),
|
||||||
|
num_events=0,
|
||||||
|
state_change_expected=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
away_val = True
|
||||||
|
events = self.verify_action(
|
||||||
|
lambda: do_update_user_status(
|
||||||
|
user_profile=cordelia,
|
||||||
|
away=away_val,
|
||||||
|
status_text="at the beach",
|
||||||
|
emoji_name=None,
|
||||||
|
emoji_code=None,
|
||||||
|
reaction_type=None,
|
||||||
|
client_id=client.id,
|
||||||
|
),
|
||||||
|
num_events=1,
|
||||||
|
state_change_expected=True,
|
||||||
|
)
|
||||||
|
check_presence(
|
||||||
|
"events[0]",
|
||||||
|
events[0],
|
||||||
|
has_email=True,
|
||||||
|
# We no longer store information about the client and we simply
|
||||||
|
# set the field to 'website' for backwards compatibility.
|
||||||
|
presence_key="website",
|
||||||
|
status="idle",
|
||||||
|
)
|
||||||
|
|
||||||
def test_user_group_events(self) -> None:
|
def test_user_group_events(self) -> None:
|
||||||
othello = self.example_user("othello")
|
othello = self.example_user("othello")
|
||||||
events = self.verify_action(
|
events = self.verify_action(
|
||||||
|
|
|
@ -37,7 +37,7 @@ class UserPresenceModelTests(ZulipTestCase):
|
||||||
|
|
||||||
user_profile = self.example_user("hamlet")
|
user_profile = self.example_user("hamlet")
|
||||||
email = user_profile.email
|
email = user_profile.email
|
||||||
presence_dct = get_presence_dict_by_realm(user_profile.realm_id)
|
presence_dct = get_presence_dict_by_realm(user_profile.realm)
|
||||||
self.assert_length(presence_dct, 0)
|
self.assert_length(presence_dct, 0)
|
||||||
|
|
||||||
self.login_user(user_profile)
|
self.login_user(user_profile)
|
||||||
|
@ -45,12 +45,12 @@ class UserPresenceModelTests(ZulipTestCase):
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
|
|
||||||
slim_presence = False
|
slim_presence = False
|
||||||
presence_dct = get_presence_dict_by_realm(user_profile.realm_id, slim_presence)
|
presence_dct = get_presence_dict_by_realm(user_profile.realm, slim_presence)
|
||||||
self.assert_length(presence_dct, 1)
|
self.assert_length(presence_dct, 1)
|
||||||
self.assertEqual(presence_dct[email]["website"]["status"], "active")
|
self.assertEqual(presence_dct[email]["website"]["status"], "active")
|
||||||
|
|
||||||
slim_presence = True
|
slim_presence = True
|
||||||
presence_dct = get_presence_dict_by_realm(user_profile.realm_id, slim_presence)
|
presence_dct = get_presence_dict_by_realm(user_profile.realm, slim_presence)
|
||||||
self.assert_length(presence_dct, 1)
|
self.assert_length(presence_dct, 1)
|
||||||
info = presence_dct[str(user_profile.id)]
|
info = presence_dct[str(user_profile.id)]
|
||||||
self.assertEqual(set(info.keys()), {"active_timestamp", "idle_timestamp"})
|
self.assertEqual(set(info.keys()), {"active_timestamp", "idle_timestamp"})
|
||||||
|
@ -64,19 +64,19 @@ class UserPresenceModelTests(ZulipTestCase):
|
||||||
|
|
||||||
# Simulate the presence being a week old first. Nothing should change.
|
# Simulate the presence being a week old first. Nothing should change.
|
||||||
back_date(num_weeks=1)
|
back_date(num_weeks=1)
|
||||||
presence_dct = get_presence_dict_by_realm(user_profile.realm_id)
|
presence_dct = get_presence_dict_by_realm(user_profile.realm)
|
||||||
self.assert_length(presence_dct, 1)
|
self.assert_length(presence_dct, 1)
|
||||||
|
|
||||||
# If the UserPresence row is three weeks old, we ignore it.
|
# If the UserPresence row is three weeks old, we ignore it.
|
||||||
back_date(num_weeks=3)
|
back_date(num_weeks=3)
|
||||||
presence_dct = get_presence_dict_by_realm(user_profile.realm_id)
|
presence_dct = get_presence_dict_by_realm(user_profile.realm)
|
||||||
self.assert_length(presence_dct, 0)
|
self.assert_length(presence_dct, 0)
|
||||||
|
|
||||||
# If the values are set to "never", ignore it just like for sufficiently old presence rows.
|
# If the values are set to "never", ignore it just like for sufficiently old presence rows.
|
||||||
UserPresence.objects.filter(id=user_profile.id).update(
|
UserPresence.objects.filter(id=user_profile.id).update(
|
||||||
last_active_time=None, last_connected_time=None
|
last_active_time=None, last_connected_time=None
|
||||||
)
|
)
|
||||||
presence_dct = get_presence_dict_by_realm(user_profile.realm_id)
|
presence_dct = get_presence_dict_by_realm(user_profile.realm)
|
||||||
self.assert_length(presence_dct, 0)
|
self.assert_length(presence_dct, 0)
|
||||||
|
|
||||||
def test_pushable_always_false(self) -> None:
|
def test_pushable_always_false(self) -> None:
|
||||||
|
@ -92,7 +92,7 @@ class UserPresenceModelTests(ZulipTestCase):
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
|
|
||||||
def pushable() -> bool:
|
def pushable() -> bool:
|
||||||
presence_dct = get_presence_dict_by_realm(user_profile.realm_id)
|
presence_dct = get_presence_dict_by_realm(user_profile.realm)
|
||||||
self.assert_length(presence_dct, 1)
|
self.assert_length(presence_dct, 1)
|
||||||
return presence_dct[email]["website"]["pushable"]
|
return presence_dct[email]["website"]["pushable"]
|
||||||
|
|
||||||
|
@ -491,6 +491,17 @@ class SingleUserPresenceTests(ZulipTestCase):
|
||||||
result = self.client_get(f"/json/users/{othello.id}/presence", subdomain="zephyr")
|
result = self.client_get(f"/json/users/{othello.id}/presence", subdomain="zephyr")
|
||||||
self.assert_json_error(result, "No such user")
|
self.assert_json_error(result, "No such user")
|
||||||
|
|
||||||
|
self.set_up_db_for_testing_user_access()
|
||||||
|
self.login("polonius")
|
||||||
|
with self.settings(CAN_ACCESS_ALL_USERS_GROUP_LIMITS_PRESENCE=True):
|
||||||
|
result = self.client_get(f"/json/users/{othello.id}/presence")
|
||||||
|
self.assert_json_error(result, "Insufficient permission")
|
||||||
|
|
||||||
|
result = self.client_get(f"/json/users/{othello.id}/presence")
|
||||||
|
result_dict = self.assert_json_success(result)
|
||||||
|
self.assertEqual(set(result_dict["presence"].keys()), {"website", "aggregated"})
|
||||||
|
self.assertEqual(set(result_dict["presence"]["website"].keys()), {"status", "timestamp"})
|
||||||
|
|
||||||
# Then, we check everything works
|
# Then, we check everything works
|
||||||
self.login("hamlet")
|
self.login("hamlet")
|
||||||
result = self.client_get("/json/users/othello@zulip.com/presence")
|
result = self.client_get("/json/users/othello@zulip.com/presence")
|
||||||
|
@ -658,6 +669,7 @@ class GetRealmStatusesTest(ZulipTestCase):
|
||||||
# Set up the test by simulating users reporting their presence data.
|
# Set up the test by simulating users reporting their presence data.
|
||||||
othello = self.example_user("othello")
|
othello = self.example_user("othello")
|
||||||
hamlet = self.example_user("hamlet")
|
hamlet = self.example_user("hamlet")
|
||||||
|
self.example_user("cordelia")
|
||||||
|
|
||||||
result = self.api_post(
|
result = self.api_post(
|
||||||
othello,
|
othello,
|
||||||
|
@ -689,6 +701,40 @@ class GetRealmStatusesTest(ZulipTestCase):
|
||||||
json = self.assert_json_success(result)
|
json = self.assert_json_success(result)
|
||||||
self.assertEqual(set(json["presences"].keys()), {hamlet.email, othello.email})
|
self.assertEqual(set(json["presences"].keys()), {hamlet.email, othello.email})
|
||||||
|
|
||||||
|
# Check that polonius cannot fetch presence data for inaccessible user Othello
|
||||||
|
# if CAN_ACCESS_ALL_USERS_GROUP_LIMITS_PRESENCE is set to True.
|
||||||
|
self.set_up_db_for_testing_user_access()
|
||||||
|
polonius = self.example_user("polonius")
|
||||||
|
with self.settings(CAN_ACCESS_ALL_USERS_GROUP_LIMITS_PRESENCE=True):
|
||||||
|
result = self.api_get(polonius, "/api/v1/realm/presence")
|
||||||
|
json = self.assert_json_success(result)
|
||||||
|
self.assertEqual(set(json["presences"].keys()), {hamlet.email})
|
||||||
|
|
||||||
|
result = self.api_get(polonius, "/api/v1/realm/presence")
|
||||||
|
json = self.assert_json_success(result)
|
||||||
|
self.assertEqual(set(json["presences"].keys()), {hamlet.email, othello.email})
|
||||||
|
|
||||||
|
with self.settings(CAN_ACCESS_ALL_USERS_GROUP_LIMITS_PRESENCE=True):
|
||||||
|
result = self.api_post(
|
||||||
|
polonius,
|
||||||
|
"/api/v1/users/me/presence",
|
||||||
|
dict(status="idle"),
|
||||||
|
HTTP_USER_AGENT="ZulipDesktop/1.0",
|
||||||
|
)
|
||||||
|
json = self.assert_json_success(result)
|
||||||
|
self.assertEqual(set(json["presences"].keys()), {hamlet.email, polonius.email})
|
||||||
|
|
||||||
|
result = self.api_post(
|
||||||
|
polonius,
|
||||||
|
"/api/v1/users/me/presence",
|
||||||
|
dict(status="idle"),
|
||||||
|
HTTP_USER_AGENT="ZulipDesktop/1.0",
|
||||||
|
)
|
||||||
|
json = self.assert_json_success(result)
|
||||||
|
self.assertEqual(
|
||||||
|
set(json["presences"].keys()), {hamlet.email, polonius.email, othello.email}
|
||||||
|
)
|
||||||
|
|
||||||
def test_presence_disabled(self) -> None:
|
def test_presence_disabled(self) -> None:
|
||||||
# Disable presence status and test whether the presence
|
# Disable presence status and test whether the presence
|
||||||
# is reported or not.
|
# is reported or not.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
|
|
||||||
|
@ -7,8 +7,10 @@ from zerver.lib.user_status import UserInfoDict, get_user_status_dict, update_us
|
||||||
from zerver.models import UserProfile, UserStatus, get_client
|
from zerver.models import UserProfile, UserStatus, get_client
|
||||||
|
|
||||||
|
|
||||||
def user_status_info(user: UserProfile) -> UserInfoDict:
|
def user_status_info(user: UserProfile, acting_user: Optional[UserProfile] = None) -> UserInfoDict:
|
||||||
user_dict = get_user_status_dict(user.realm_id)
|
if acting_user is None:
|
||||||
|
acting_user = user
|
||||||
|
user_dict = get_user_status_dict(user.realm, acting_user)
|
||||||
return user_dict.get(str(user.id), {})
|
return user_dict.get(str(user.id), {})
|
||||||
|
|
||||||
|
|
||||||
|
@ -115,6 +117,26 @@ class UserStatusTest(ZulipTestCase):
|
||||||
dict(status_text="in a meeting"),
|
dict(status_text="in a meeting"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Test user status for inaccessible users.
|
||||||
|
self.set_up_db_for_testing_user_access()
|
||||||
|
cordelia = self.example_user("cordelia")
|
||||||
|
update_user_status(
|
||||||
|
user_profile_id=cordelia.id,
|
||||||
|
status_text="on vacation",
|
||||||
|
emoji_name=None,
|
||||||
|
emoji_code=None,
|
||||||
|
reaction_type=None,
|
||||||
|
client_id=client2.id,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
user_status_info(hamlet, self.example_user("polonius")),
|
||||||
|
dict(status_text="in a meeting"),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
user_status_info(cordelia, self.example_user("polonius")),
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
def update_status_and_assert_event(
|
def update_status_and_assert_event(
|
||||||
self, payload: Dict[str, Any], expected_event: Dict[str, Any], num_events: int = 1
|
self, payload: Dict[str, Any], expected_event: Dict[str, Any], num_events: int = 1
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -125,7 +147,7 @@ class UserStatusTest(ZulipTestCase):
|
||||||
|
|
||||||
def test_endpoints(self) -> None:
|
def test_endpoints(self) -> None:
|
||||||
hamlet = self.example_user("hamlet")
|
hamlet = self.example_user("hamlet")
|
||||||
realm_id = hamlet.realm_id
|
realm = hamlet.realm
|
||||||
|
|
||||||
self.login_user(hamlet)
|
self.login_user(hamlet)
|
||||||
|
|
||||||
|
@ -263,7 +285,7 @@ class UserStatusTest(ZulipTestCase):
|
||||||
expected_event=dict(type="user_status", user_id=hamlet.id, status_text=""),
|
expected_event=dict(type="user_status", user_id=hamlet.id, status_text=""),
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
get_user_status_dict(realm_id=realm_id),
|
get_user_status_dict(realm=realm, user_profile=hamlet),
|
||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ from zerver.lib.request import RequestNotes
|
||||||
from zerver.lib.response import json_success
|
from zerver.lib.response import json_success
|
||||||
from zerver.lib.timestamp import datetime_to_timestamp
|
from zerver.lib.timestamp import datetime_to_timestamp
|
||||||
from zerver.lib.typed_endpoint import ApiParamConfig, typed_endpoint
|
from zerver.lib.typed_endpoint import ApiParamConfig, typed_endpoint
|
||||||
|
from zerver.lib.users import check_can_access_user
|
||||||
from zerver.models import (
|
from zerver.models import (
|
||||||
UserActivity,
|
UserActivity,
|
||||||
UserPresence,
|
UserPresence,
|
||||||
|
@ -48,6 +49,11 @@ def get_presence_backend(
|
||||||
if target.is_bot:
|
if target.is_bot:
|
||||||
raise JsonableError(_("Presence is not supported for bot users."))
|
raise JsonableError(_("Presence is not supported for bot users."))
|
||||||
|
|
||||||
|
if settings.CAN_ACCESS_ALL_USERS_GROUP_LIMITS_PRESENCE and not check_can_access_user(
|
||||||
|
target, user_profile
|
||||||
|
):
|
||||||
|
raise JsonableError(_("Insufficient permission"))
|
||||||
|
|
||||||
presence_dict = get_presence_for_user(target.id)
|
presence_dict = get_presence_for_user(target.id)
|
||||||
if len(presence_dict) == 0:
|
if len(presence_dict) == 0:
|
||||||
raise JsonableError(
|
raise JsonableError(
|
||||||
|
|
|
@ -601,3 +601,10 @@ TYPING_STARTED_WAIT_PERIOD_MILLISECONDS = 10000
|
||||||
# notifications enabled. Default is set to avoid excessive Tornado
|
# notifications enabled. Default is set to avoid excessive Tornado
|
||||||
# load in large organizations.
|
# load in large organizations.
|
||||||
MAX_STREAM_SIZE_FOR_TYPING_NOTIFICATIONS = 100
|
MAX_STREAM_SIZE_FOR_TYPING_NOTIFICATIONS = 100
|
||||||
|
|
||||||
|
# Limiting guest access to other users via the
|
||||||
|
# can_access_all_users_group setting makes presence queries much more
|
||||||
|
# expensive. This can be a significant performance problem for
|
||||||
|
# installations with thousands of users with many guests limited in
|
||||||
|
# this way, pending further optimization of the relevant code paths.
|
||||||
|
CAN_ACCESS_ALL_USERS_GROUP_LIMITS_PRESENCE = False
|
||||||
|
|
Loading…
Reference in New Issue