streams: Allow unsubscribing others based on can_remove_subscribers_group.

Current value of can_remove_subscribers_group field is admins system group
only so behavior is not changed. We would provide support to change this
setting from API and UI in further commits.
This commit is contained in:
Sahil Batra 2022-07-13 22:17:44 +05:30 committed by Tim Abbott
parent b9248c75f4
commit c3fe8420fd
2 changed files with 81 additions and 12 deletions

View File

@ -19,6 +19,7 @@ from zerver.lib.stream_subscription import (
) )
from zerver.lib.string_validation import check_stream_name from zerver.lib.string_validation import check_stream_name
from zerver.lib.types import APIStreamDict from zerver.lib.types import APIStreamDict
from zerver.lib.user_groups import is_user_in_group
from zerver.models import ( from zerver.models import (
DefaultStreamGroup, DefaultStreamGroup,
Realm, Realm,
@ -604,6 +605,17 @@ def can_access_stream_history_by_id(user_profile: UserProfile, stream_id: int) -
return can_access_stream_history(user_profile, stream) return can_access_stream_history(user_profile, stream)
def can_remove_subscribers_from_stream(
stream: Stream, user_profile: UserProfile, sub: Subscription
) -> bool:
if not check_basic_stream_access(user_profile, stream, sub, allow_realm_admin=True):
return False
group_allowed_to_remove_subscribers = stream.can_remove_subscribers_group
assert group_allowed_to_remove_subscribers is not None
return is_user_in_group(group_allowed_to_remove_subscribers, user_profile)
def filter_stream_authorization( def filter_stream_authorization(
user_profile: UserProfile, streams: Collection[Stream] user_profile: UserProfile, streams: Collection[Stream]
) -> Tuple[List[Stream], List[Stream]]: ) -> Tuple[List[Stream], List[Stream]]:
@ -679,7 +691,8 @@ def list_to_streams(
sub_map = {sub.recipient_id: sub for sub in subs} sub_map = {sub.recipient_id: sub for sub in subs}
for stream in existing_stream_map.values(): for stream in existing_stream_map.values():
sub = sub_map.get(stream.recipient_id, None) sub = sub_map.get(stream.recipient_id, None)
check_stream_access_for_delete_or_update(user_profile, stream, sub) if not can_remove_subscribers_from_stream(stream, user_profile, sub):
raise JsonableError(_("Insufficient permission"))
message_retention_days_not_none = False message_retention_days_not_none = False
web_public_stream_requested = False web_public_stream_requested = False

View File

@ -28,9 +28,11 @@ from zerver.actions.realm_settings import do_change_realm_plan_type, do_set_real
from zerver.actions.streams import ( from zerver.actions.streams import (
bulk_add_subscriptions, bulk_add_subscriptions,
bulk_remove_subscriptions, bulk_remove_subscriptions,
do_change_can_remove_subscribers_group,
do_change_stream_post_policy, do_change_stream_post_policy,
do_deactivate_stream, do_deactivate_stream,
) )
from zerver.actions.user_groups import add_subgroups_to_user_group
from zerver.actions.users import do_change_user_role, do_deactivate_user from zerver.actions.users import do_change_user_role, do_deactivate_user
from zerver.lib.exceptions import JsonableError from zerver.lib.exceptions import JsonableError
from zerver.lib.message import UnreadStreamInfo, aggregate_unread_data, get_raw_unread_data from zerver.lib.message import UnreadStreamInfo, aggregate_unread_data, get_raw_unread_data
@ -80,6 +82,7 @@ from zerver.lib.types import (
NeverSubscribedStreamDict, NeverSubscribedStreamDict,
SubscriptionInfo, SubscriptionInfo,
) )
from zerver.lib.user_groups import create_user_group
from zerver.models import ( from zerver.models import (
Attachment, Attachment,
DefaultStream, DefaultStream,
@ -90,6 +93,7 @@ from zerver.models import (
Recipient, Recipient,
Stream, Stream,
Subscription, Subscription,
UserGroup,
UserMessage, UserMessage,
UserProfile, UserProfile,
active_non_guest_user_ids, active_non_guest_user_ids,
@ -2237,14 +2241,14 @@ class StreamAdminTest(ZulipTestCase):
If you're not an admin, you can't remove other people from streams. If you're not an admin, you can't remove other people from streams.
""" """
result = self.attempt_unsubscribe_of_principal( result = self.attempt_unsubscribe_of_principal(
query_count=5, query_count=6,
target_users=[self.example_user("cordelia")], target_users=[self.example_user("cordelia")],
is_realm_admin=False, is_realm_admin=False,
is_subbed=True, is_subbed=True,
invite_only=False, invite_only=False,
target_users_subbed=True, target_users_subbed=True,
) )
self.assert_json_error(result, "Must be an organization administrator") self.assert_json_error(result, "Insufficient permission")
def test_realm_admin_remove_others_from_public_stream(self) -> None: def test_realm_admin_remove_others_from_public_stream(self) -> None:
""" """
@ -2252,7 +2256,7 @@ class StreamAdminTest(ZulipTestCase):
those you aren't on. those you aren't on.
""" """
result = self.attempt_unsubscribe_of_principal( result = self.attempt_unsubscribe_of_principal(
query_count=14, query_count=15,
target_users=[self.example_user("cordelia")], target_users=[self.example_user("cordelia")],
is_realm_admin=True, is_realm_admin=True,
is_subbed=True, is_subbed=True,
@ -2278,7 +2282,7 @@ class StreamAdminTest(ZulipTestCase):
self.example_user(name) for name in ["cordelia", "prospero", "iago", "hamlet", "ZOE"] self.example_user(name) for name in ["cordelia", "prospero", "iago", "hamlet", "ZOE"]
] ]
result = self.attempt_unsubscribe_of_principal( result = self.attempt_unsubscribe_of_principal(
query_count=25, query_count=26,
cache_count=9, cache_count=9,
target_users=target_users, target_users=target_users,
is_realm_admin=True, is_realm_admin=True,
@ -2296,7 +2300,7 @@ class StreamAdminTest(ZulipTestCase):
are on. are on.
""" """
result = self.attempt_unsubscribe_of_principal( result = self.attempt_unsubscribe_of_principal(
query_count=15, query_count=16,
target_users=[self.example_user("cordelia")], target_users=[self.example_user("cordelia")],
is_realm_admin=True, is_realm_admin=True,
is_subbed=True, is_subbed=True,
@ -2313,7 +2317,7 @@ class StreamAdminTest(ZulipTestCase):
streams you aren't on. streams you aren't on.
""" """
result = self.attempt_unsubscribe_of_principal( result = self.attempt_unsubscribe_of_principal(
query_count=15, query_count=16,
target_users=[self.example_user("cordelia")], target_users=[self.example_user("cordelia")],
is_realm_admin=True, is_realm_admin=True,
is_subbed=False, is_subbed=False,
@ -2327,7 +2331,7 @@ class StreamAdminTest(ZulipTestCase):
def test_cant_remove_others_from_stream_legacy_emails(self) -> None: def test_cant_remove_others_from_stream_legacy_emails(self) -> None:
result = self.attempt_unsubscribe_of_principal( result = self.attempt_unsubscribe_of_principal(
query_count=5, query_count=6,
is_realm_admin=False, is_realm_admin=False,
is_subbed=True, is_subbed=True,
invite_only=False, invite_only=False,
@ -2335,11 +2339,11 @@ class StreamAdminTest(ZulipTestCase):
target_users_subbed=True, target_users_subbed=True,
using_legacy_emails=True, using_legacy_emails=True,
) )
self.assert_json_error(result, "Must be an organization administrator") self.assert_json_error(result, "Insufficient permission")
def test_admin_remove_others_from_stream_legacy_emails(self) -> None: def test_admin_remove_others_from_stream_legacy_emails(self) -> None:
result = self.attempt_unsubscribe_of_principal( result = self.attempt_unsubscribe_of_principal(
query_count=14, query_count=15,
target_users=[self.example_user("cordelia")], target_users=[self.example_user("cordelia")],
is_realm_admin=True, is_realm_admin=True,
is_subbed=True, is_subbed=True,
@ -2353,7 +2357,7 @@ class StreamAdminTest(ZulipTestCase):
def test_admin_remove_multiple_users_from_stream_legacy_emails(self) -> None: def test_admin_remove_multiple_users_from_stream_legacy_emails(self) -> None:
result = self.attempt_unsubscribe_of_principal( result = self.attempt_unsubscribe_of_principal(
query_count=17, query_count=18,
target_users=[self.example_user("cordelia"), self.example_user("prospero")], target_users=[self.example_user("cordelia"), self.example_user("prospero")],
is_realm_admin=True, is_realm_admin=True,
is_subbed=True, is_subbed=True,
@ -2371,7 +2375,7 @@ class StreamAdminTest(ZulipTestCase):
fails gracefully. fails gracefully.
""" """
result = self.attempt_unsubscribe_of_principal( result = self.attempt_unsubscribe_of_principal(
query_count=9, query_count=10,
target_users=[self.example_user("cordelia")], target_users=[self.example_user("cordelia")],
is_realm_admin=True, is_realm_admin=True,
is_subbed=False, is_subbed=False,
@ -2382,6 +2386,58 @@ class StreamAdminTest(ZulipTestCase):
self.assert_length(json["removed"], 0) self.assert_length(json["removed"], 0)
self.assert_length(json["not_removed"], 1) self.assert_length(json["not_removed"], 1)
def test_can_remove_subscribers_group(self) -> None:
realm = get_realm("zulip")
leadership_group = create_user_group(
"leadership", [self.example_user("iago"), self.example_user("shiva")], realm
)
managers_group = create_user_group("managers", [self.example_user("hamlet")], realm=realm)
add_subgroups_to_user_group(managers_group, [leadership_group])
cordelia = self.example_user("cordelia")
stream = self.make_stream("public_stream")
def check_unsubscribing_user(
user: UserProfile, can_remove_subscribers_group: UserGroup, expect_fail: bool = False
) -> None:
self.login_user(user)
self.subscribe(cordelia, stream.name)
do_change_can_remove_subscribers_group(
stream, can_remove_subscribers_group, acting_user=None
)
result = self.client_delete(
"/json/users/me/subscriptions",
{
"subscriptions": orjson.dumps([stream.name]).decode(),
"principals": orjson.dumps([cordelia.id]).decode(),
},
)
if expect_fail:
self.assert_json_error(result, "Insufficient permission")
return
json = self.assert_json_success(result)
self.assert_length(json["removed"], 1)
self.assert_length(json["not_removed"], 0)
check_unsubscribing_user(self.example_user("hamlet"), leadership_group, expect_fail=True)
check_unsubscribing_user(self.example_user("desdemona"), leadership_group, expect_fail=True)
check_unsubscribing_user(self.example_user("iago"), leadership_group)
check_unsubscribing_user(self.example_user("othello"), managers_group, expect_fail=True)
check_unsubscribing_user(self.example_user("shiva"), managers_group)
check_unsubscribing_user(self.example_user("hamlet"), managers_group)
stream = self.make_stream("private_stream", invite_only=True)
self.subscribe(self.example_user("hamlet"), stream.name)
# Non-admins are not allowed to unsubscribe others from private streams that they
# are not subscribed to even if they are member of the allowed group.
check_unsubscribing_user(self.example_user("shiva"), leadership_group, expect_fail=True)
check_unsubscribing_user(self.example_user("iago"), leadership_group)
self.subscribe(self.example_user("shiva"), stream.name)
check_unsubscribing_user(self.example_user("shiva"), leadership_group)
def test_remove_invalid_user(self) -> None: def test_remove_invalid_user(self) -> None:
""" """
Trying to unsubscribe an invalid user from a stream fails gracefully. Trying to unsubscribe an invalid user from a stream fails gracefully.