users: Send stream creation/deletion events on role change.

We now send stream creation and stream deletion events on
changing a user's role because a user can gain or lose
access to some streams on changing their role.
This commit is contained in:
Sahil Batra 2023-07-06 12:08:12 +05:30 committed by Tim Abbott
parent 5e3c39ea4f
commit ada2991f1c
7 changed files with 157 additions and 31 deletions

View File

@ -24,6 +24,9 @@ format used by the Zulip server that they are interacting with.
* [`POST /register`](/api/register-queue): `streams` field in the response * [`POST /register`](/api/register-queue): `streams` field in the response
now included web-public streams as well. now included web-public streams as well.
* [`GET /events`](/api/get-events): Events for stream creation and deletion
are now sent to the user if they gain or lose access to some streams due
to change in their role.
**Feature level 204** **Feature level 204**

View File

@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
# Changes should be accompanied by documentation explaining what the # Changes should be accompanied by documentation explaining what the
# new level means in api_docs/changelog.md, as well as "**Changes**" # new level means in api_docs/changelog.md, as well as "**Changes**"
# entries in the endpoint's documentation in `zulip.yaml`. # entries in the endpoint's documentation in `zulip.yaml`.
API_FEATURE_LEVEL = 204 API_FEATURE_LEVEL = 205
# Bump the minor PROVISION_VERSION to indicate that folks should provision # Bump the minor PROVISION_VERSION to indicate that folks should provision
# only when going from an old version of the code to a newer version. Bump # only when going from an old version of the code to a newer version. Bump

View File

@ -19,6 +19,9 @@ from zerver.lib.cache import bot_dict_fields
from zerver.lib.create_user import create_user from zerver.lib.create_user import create_user
from zerver.lib.send_email import clear_scheduled_emails from zerver.lib.send_email import clear_scheduled_emails
from zerver.lib.sessions import delete_user_sessions from zerver.lib.sessions import delete_user_sessions
from zerver.lib.stream_subscription import bulk_get_subscriber_peer_info
from zerver.lib.stream_traffic import get_streams_traffic
from zerver.lib.streams import get_streams_for_user, stream_to_dict
from zerver.lib.user_counts import realm_user_count_by_role from zerver.lib.user_counts import realm_user_count_by_role
from zerver.lib.user_groups import get_system_user_group_for_user from zerver.lib.user_groups import get_system_user_group_for_user
from zerver.lib.users import get_active_bots_owned_by_user from zerver.lib.users import get_active_bots_owned_by_user
@ -28,6 +31,7 @@ from zerver.models import (
RealmAuditLog, RealmAuditLog,
Recipient, Recipient,
Service, Service,
Stream,
Subscription, Subscription,
UserGroupMembership, UserGroupMembership,
UserProfile, UserProfile,
@ -309,6 +313,59 @@ def do_deactivate_user(
) )
def send_stream_events_for_role_update(
user_profile: UserProfile, old_accessible_streams: List[Stream]
) -> None:
current_accessible_streams = get_streams_for_user(
user_profile,
include_all_active=user_profile.is_realm_admin,
include_web_public=True,
)
old_accessible_stream_ids = {stream.id for stream in old_accessible_streams}
current_accessible_stream_ids = {stream.id for stream in current_accessible_streams}
now_accessible_stream_ids = current_accessible_stream_ids - old_accessible_stream_ids
if now_accessible_stream_ids:
recent_traffic = get_streams_traffic(now_accessible_stream_ids, user_profile.realm)
now_accessible_streams = [
stream
for stream in current_accessible_streams
if stream.id in now_accessible_stream_ids
]
event = dict(
type="stream",
op="create",
streams=[stream_to_dict(stream, recent_traffic) for stream in now_accessible_streams],
)
send_event_on_commit(user_profile.realm, event, [user_profile.id])
subscriber_peer_info = bulk_get_subscriber_peer_info(
user_profile.realm, now_accessible_streams
)
for stream_id, stream_subscriber_set in subscriber_peer_info.subscribed_ids.items():
peer_add_event = dict(
type="subscription",
op="peer_add",
stream_ids=[stream_id],
user_ids=sorted(stream_subscriber_set),
)
send_event_on_commit(user_profile.realm, peer_add_event, [user_profile.id])
now_inaccessible_stream_ids = old_accessible_stream_ids - current_accessible_stream_ids
if now_inaccessible_stream_ids:
now_inaccessible_streams = [
stream for stream in old_accessible_streams if stream.id in now_inaccessible_stream_ids
]
event = dict(
type="stream",
op="delete",
streams=[stream_to_dict(stream) for stream in now_inaccessible_streams],
)
send_event_on_commit(user_profile.realm, event, [user_profile.id])
@transaction.atomic(durable=True) @transaction.atomic(durable=True)
def do_change_user_role( def do_change_user_role(
user_profile: UserProfile, value: int, *, acting_user: Optional[UserProfile] user_profile: UserProfile, value: int, *, acting_user: Optional[UserProfile]
@ -316,6 +373,12 @@ def do_change_user_role(
old_value = user_profile.role old_value = user_profile.role
old_system_group = get_system_user_group_for_user(user_profile) old_system_group = get_system_user_group_for_user(user_profile)
previously_accessible_streams = get_streams_for_user(
user_profile,
include_web_public=True,
include_all_active=user_profile.is_realm_admin,
)
user_profile.role = value user_profile.role = value
user_profile.save(update_fields=["role"]) user_profile.save(update_fields=["role"])
RealmAuditLog.objects.create( RealmAuditLog.objects.create(
@ -372,6 +435,8 @@ def do_change_user_role(
user_profile.realm, [user_profile.id], acting_user=acting_user user_profile.realm, [user_profile.id], acting_user=acting_user
) )
send_stream_events_for_role_update(user_profile, previously_accessible_streams)
def do_make_user_billing_admin(user_profile: UserProfile) -> None: def do_make_user_billing_admin(user_profile: UserProfile) -> None:
user_profile.is_billing_admin = True user_profile.is_billing_admin = True

View File

@ -882,26 +882,6 @@ def apply_event(
state["can_subscribe_other_users"] = user_profile.can_subscribe_other_users() state["can_subscribe_other_users"] = user_profile.can_subscribe_other_users()
state["can_invite_others_to_realm"] = user_profile.can_invite_others_to_realm() state["can_invite_others_to_realm"] = user_profile.can_invite_others_to_realm()
# TODO: Probably rather than writing the perfect
# live-update code for the case of racing with the
# current user changing roles, we should just do a
# full refetch.
if "never_subscribed" in state:
sub_info = gather_subscriptions_helper(
user_profile,
include_subscribers=include_subscribers,
)
state["subscriptions"] = sub_info.subscriptions
state["unsubscribed"] = sub_info.unsubscribed
state["never_subscribed"] = sub_info.never_subscribed
if "streams" in state:
state["streams"] = do_get_streams(
user_profile,
include_web_public=True,
include_all_active=user_profile.is_realm_admin,
)
if state["is_guest"]: if state["is_guest"]:
state["realm_default_streams"] = [] state["realm_default_streams"] = []
else: else:

View File

@ -1165,12 +1165,16 @@ paths:
This event is also sent when a user gains access to a stream they This event is also sent when a user gains access to a stream they
previously [could not access](/help/stream-permissions), such as previously [could not access](/help/stream-permissions), such as
when a private stream is made public, or when a [guest user](/help/roles-and-permissions) when a private stream is made public, their role changes or when
is subscribed to a public (or private) stream. a [guest user](/help/roles-and-permissions) is subscribed to a
public (or private) stream.
Note that organization administrators who are not subscribed will Note that organization administrators who are not subscribed will
not be able to see content on the stream; just that it exists. not be able to see content on the stream; just that it exists.
**Changes**: Prior to Zulip 8.0 (feature level 205), this event was
not sent when a user gained access to a stream due to role change.
**Changes**: Prior to Zulip 8.0 (feature level 192), this event was **Changes**: Prior to Zulip 8.0 (feature level 192), this event was
not sent when guest users gained access to a public stream by being not sent when guest users gained access to a public stream by being
subscribed. subscribed.
@ -1222,6 +1226,13 @@ paths:
- type: object - type: object
description: | description: |
Event sent to all users who can see a stream when it is deactivated. Event sent to all users who can see a stream when it is deactivated.
This event is also sent when a user loses access to a stream they
previously [could access](/help/stream-permissions) when their roles
changes.
**Changes**: Prior to Zulip 8.0 (feature level 205), this event was
not sent when a user lost access to a stream due to role change.
properties: properties:
id: id:
$ref: "#/components/schemas/EventIdSchema" $ref: "#/components/schemas/EventIdSchema"

View File

@ -1859,10 +1859,19 @@ class NormalActionsTest(BaseAction):
self.user_profile.refresh_from_db() self.user_profile.refresh_from_db()
do_change_user_role(self.user_profile, UserProfile.ROLE_MEMBER, acting_user=None) do_change_user_role(self.user_profile, UserProfile.ROLE_MEMBER, acting_user=None)
self.make_stream("Test private stream", invite_only=True)
self.subscribe(self.example_user("othello"), "Test private stream")
for role in [UserProfile.ROLE_REALM_ADMINISTRATOR, UserProfile.ROLE_MEMBER]: for role in [UserProfile.ROLE_REALM_ADMINISTRATOR, UserProfile.ROLE_MEMBER]:
if role == UserProfile.ROLE_REALM_ADMINISTRATOR:
num_events = 6
else:
num_events = 5
events = self.verify_action( events = self.verify_action(
lambda: do_change_user_role(self.user_profile, role, acting_user=None), lambda: do_change_user_role(self.user_profile, role, acting_user=None),
num_events=4, num_events=num_events,
) )
check_realm_user_update("events[0]", events[0], "role") check_realm_user_update("events[0]", events[0], "role")
self.assertEqual(events[0]["person"]["role"], role) self.assertEqual(events[0]["person"]["role"], role)
@ -1872,8 +1881,11 @@ class NormalActionsTest(BaseAction):
if role == UserProfile.ROLE_REALM_ADMINISTRATOR: if role == UserProfile.ROLE_REALM_ADMINISTRATOR:
check_user_group_remove_members("events[3]", events[3]) check_user_group_remove_members("events[3]", events[3])
check_stream_create("events[4]", events[4])
check_subscription_peer_add("events[5]", events[5])
else: else:
check_user_group_add_members("events[3]", events[3]) check_user_group_add_members("events[3]", events[3])
check_stream_delete("events[4]", events[4])
def test_change_is_billing_admin(self) -> None: def test_change_is_billing_admin(self) -> None:
reset_email_visibility_to_everyone_in_zulip_realm() reset_email_visibility_to_everyone_in_zulip_realm()
@ -1896,10 +1908,18 @@ class NormalActionsTest(BaseAction):
self.user_profile.refresh_from_db() self.user_profile.refresh_from_db()
do_change_user_role(self.user_profile, UserProfile.ROLE_MEMBER, acting_user=None) do_change_user_role(self.user_profile, UserProfile.ROLE_MEMBER, acting_user=None)
self.make_stream("Test private stream", invite_only=True)
self.subscribe(self.example_user("othello"), "Test private stream")
for role in [UserProfile.ROLE_REALM_OWNER, UserProfile.ROLE_MEMBER]: for role in [UserProfile.ROLE_REALM_OWNER, UserProfile.ROLE_MEMBER]:
if role == UserProfile.ROLE_REALM_OWNER:
num_events = 6
else:
num_events = 5
events = self.verify_action( events = self.verify_action(
lambda: do_change_user_role(self.user_profile, role, acting_user=None), lambda: do_change_user_role(self.user_profile, role, acting_user=None),
num_events=4, num_events=num_events,
) )
check_realm_user_update("events[0]", events[0], "role") check_realm_user_update("events[0]", events[0], "role")
self.assertEqual(events[0]["person"]["role"], role) self.assertEqual(events[0]["person"]["role"], role)
@ -1909,8 +1929,11 @@ class NormalActionsTest(BaseAction):
if role == UserProfile.ROLE_REALM_OWNER: if role == UserProfile.ROLE_REALM_OWNER:
check_user_group_remove_members("events[3]", events[3]) check_user_group_remove_members("events[3]", events[3])
check_stream_create("events[4]", events[4])
check_subscription_peer_add("events[5]", events[5])
else: else:
check_user_group_add_members("events[3]", events[3]) check_user_group_add_members("events[3]", events[3])
check_stream_delete("events[4]", events[4])
def test_change_is_moderator(self) -> None: def test_change_is_moderator(self) -> None:
reset_email_visibility_to_everyone_in_zulip_realm() reset_email_visibility_to_everyone_in_zulip_realm()
@ -1950,9 +1973,16 @@ class NormalActionsTest(BaseAction):
do_change_user_role(self.user_profile, UserProfile.ROLE_MEMBER, acting_user=None) do_change_user_role(self.user_profile, UserProfile.ROLE_MEMBER, acting_user=None)
for role in [UserProfile.ROLE_GUEST, UserProfile.ROLE_MEMBER]: for role in [UserProfile.ROLE_GUEST, UserProfile.ROLE_MEMBER]:
if role == UserProfile.ROLE_MEMBER:
# When changing role from guest to member, peer_add events are also sent
# to make sure the subscribers info is provided to the clients for the
# streams added by stream creation event.
num_events = 7
else:
num_events = 5
events = self.verify_action( events = self.verify_action(
lambda: do_change_user_role(self.user_profile, role, acting_user=None), lambda: do_change_user_role(self.user_profile, role, acting_user=None),
num_events=4, num_events=num_events,
) )
check_realm_user_update("events[0]", events[0], "role") check_realm_user_update("events[0]", events[0], "role")
self.assertEqual(events[0]["person"]["role"], role) self.assertEqual(events[0]["person"]["role"], role)
@ -1962,8 +1992,12 @@ class NormalActionsTest(BaseAction):
if role == UserProfile.ROLE_GUEST: if role == UserProfile.ROLE_GUEST:
check_user_group_remove_members("events[3]", events[3]) check_user_group_remove_members("events[3]", events[3])
check_stream_delete("events[4]", events[4])
else: else:
check_user_group_add_members("events[3]", events[3]) check_user_group_add_members("events[3]", events[3])
check_stream_create("events[4]", events[4])
check_subscription_peer_add("events[5]", events[5])
check_subscription_peer_add("events[6]", events[6])
def test_change_notification_settings(self) -> None: def test_change_notification_settings(self) -> None:
for notification_setting in self.user_profile.notification_setting_types: for notification_setting in self.user_profile.notification_setting_types:

View File

@ -220,7 +220,7 @@ class PermissionTest(ZulipTestCase):
self.assertFalse(othello_dict["is_owner"]) self.assertFalse(othello_dict["is_owner"])
req = dict(role=UserProfile.ROLE_REALM_OWNER) req = dict(role=UserProfile.ROLE_REALM_OWNER)
with self.capture_send_event_calls(expected_num_events=4) as events: with self.capture_send_event_calls(expected_num_events=6) as events:
result = self.client_patch(f"/json/users/{othello.id}", req) result = self.client_patch(f"/json/users/{othello.id}", req)
self.assert_json_success(result) self.assert_json_success(result)
owner_users = realm.get_human_owner_users() owner_users = realm.get_human_owner_users()
@ -230,7 +230,7 @@ class PermissionTest(ZulipTestCase):
self.assertEqual(person["role"], UserProfile.ROLE_REALM_OWNER) self.assertEqual(person["role"], UserProfile.ROLE_REALM_OWNER)
req = dict(role=UserProfile.ROLE_MEMBER) req = dict(role=UserProfile.ROLE_MEMBER)
with self.capture_send_event_calls(expected_num_events=4) as events: with self.capture_send_event_calls(expected_num_events=5) as events:
result = self.client_patch(f"/json/users/{othello.id}", req) result = self.client_patch(f"/json/users/{othello.id}", req)
self.assert_json_success(result) self.assert_json_success(result)
owner_users = realm.get_human_owner_users() owner_users = realm.get_human_owner_users()
@ -281,7 +281,7 @@ class PermissionTest(ZulipTestCase):
# Giveth # Giveth
req = dict(role=orjson.dumps(UserProfile.ROLE_REALM_ADMINISTRATOR).decode()) req = dict(role=orjson.dumps(UserProfile.ROLE_REALM_ADMINISTRATOR).decode())
with self.capture_send_event_calls(expected_num_events=4) as events: with self.capture_send_event_calls(expected_num_events=6) as events:
result = self.client_patch(f"/json/users/{othello.id}", req) result = self.client_patch(f"/json/users/{othello.id}", req)
self.assert_json_success(result) self.assert_json_success(result)
admin_users = realm.get_human_admin_users() admin_users = realm.get_human_admin_users()
@ -292,7 +292,7 @@ class PermissionTest(ZulipTestCase):
# Taketh away # Taketh away
req = dict(role=orjson.dumps(UserProfile.ROLE_MEMBER).decode()) req = dict(role=orjson.dumps(UserProfile.ROLE_MEMBER).decode())
with self.capture_send_event_calls(expected_num_events=4) as events: with self.capture_send_event_calls(expected_num_events=5) as events:
result = self.client_patch(f"/json/users/{othello.id}", req) result = self.client_patch(f"/json/users/{othello.id}", req)
self.assert_json_success(result) self.assert_json_success(result)
admin_users = realm.get_human_admin_users() admin_users = realm.get_human_admin_users()
@ -518,9 +518,42 @@ class PermissionTest(ZulipTestCase):
) )
req = dict(role=orjson.dumps(new_role).decode()) req = dict(role=orjson.dumps(new_role).decode())
# The basic events sent in all cases on changing role are - one event
# for changing role and one event each for adding and removing user
# from system user group.
num_events = 3 num_events = 3
if UserProfile.ROLE_MEMBER in [old_role, new_role]: if UserProfile.ROLE_MEMBER in [old_role, new_role]:
num_events = 4 # There is one additional event for adding/removing user from
# the "Full members" group as well.
num_events += 1
if new_role == UserProfile.ROLE_GUEST:
# There is one additional event deleting the unsubscribed public
# streams that the user will not able to access after becoming guest.
num_events += 1
if old_role == UserProfile.ROLE_GUEST:
# User will receive one event for creation of unsubscribed public
# (and private, if the new role is owner or admin) streams that
# they did not have access to previously and 3 more peer_add
# events for each of the public stream.
if new_role in [UserProfile.ROLE_REALM_ADMINISTRATOR, UserProfile.ROLE_REALM_OWNER]:
# If the new role is owner or admin, the peer_add event will be
# sent for one private stream as well.
num_events += 5
else:
num_events += 4
elif new_role in [
UserProfile.ROLE_REALM_ADMINISTRATOR,
UserProfile.ROLE_REALM_OWNER,
] and old_role not in [UserProfile.ROLE_REALM_ADMINISTRATOR, UserProfile.ROLE_REALM_OWNER]:
# If old_role is not guest and user's role is changed to admin or owner from moderator
# or member, then the user gains access to unsubscribed private streams and thus
# receives one event for stream creation and one peer_add event for it.
num_events += 2
with self.capture_send_event_calls(expected_num_events=num_events) as events: with self.capture_send_event_calls(expected_num_events=num_events) as events:
result = self.client_patch(f"/json/users/{user_profile.id}", req) result = self.client_patch(f"/json/users/{user_profile.id}", req)
self.assert_json_success(result) self.assert_json_success(result)