From 62478f900d743574ca5b2a3fc44ea939eff28f17 Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Wed, 19 Feb 2025 16:05:31 +0530 Subject: [PATCH] streams: Modfiy stream permissions to use can_subscribe_group. Fixes part of #33417. --- zerver/lib/streams.py | 27 ++- zerver/lib/subscription_info.py | 8 + zerver/models/streams.py | 2 + zerver/tests/test_subs.py | 290 ++++++++++++++++++++++++++++++-- 4 files changed, 307 insertions(+), 20 deletions(-) diff --git a/zerver/lib/streams.py b/zerver/lib/streams.py index e884605b01..9a2e8b0881 100644 --- a/zerver/lib/streams.py +++ b/zerver/lib/streams.py @@ -194,8 +194,11 @@ def get_users_dict_with_metadata_access_to_streams_via_permission_groups( ) -> dict[int, set[int]]: can_administer_group_ids = {stream.can_administer_channel_group_id for stream in streams} can_add_subscriber_group_ids = {stream.can_add_subscribers_group_id for stream in streams} + can_subscribe_group_ids = {stream.can_subscribe_group_id for stream in streams} - all_permission_group_ids = list(can_administer_group_ids | can_add_subscriber_group_ids) + all_permission_group_ids = list( + can_administer_group_ids | can_add_subscriber_group_ids | can_subscribe_group_ids + ) recursive_subgroups = get_root_id_annotated_recursive_subgroups_for_groups( all_permission_group_ids, realm_id @@ -229,6 +232,7 @@ def get_users_dict_with_metadata_access_to_streams_via_permission_groups( users_with_metadata_access_dict[stream.id] = ( group_members_dict[stream.can_administer_channel_group_id] | group_members_dict[stream.can_add_subscribers_group_id] + | group_members_dict[stream.can_subscribe_group_id] ) return users_with_metadata_access_dict @@ -416,13 +420,23 @@ def is_user_in_can_add_subscribers_group( return group_allowed_to_add_subscribers_id in user_recursive_group_ids +def is_user_in_can_subscribe_group(stream: Stream, user_recursive_group_ids: set[int]) -> bool: + # Important: The caller must have verified the acting user + # is not a guest, to enforce that can_subscribe_group has + # allow_everyone_group=False. + group_allowed_to_subscribe_id = stream.can_subscribe_group_id + return group_allowed_to_subscribe_id in user_recursive_group_ids + + def is_user_in_groups_granting_content_access( stream: Stream, user_recursive_group_ids: set[int] ) -> bool: # Important: The caller must have verified the acting user is not # a guest, to enforce that can_add_subscribers_group has # allow_everyone_group=False. - return is_user_in_can_add_subscribers_group(stream, user_recursive_group_ids) + return is_user_in_can_subscribe_group( + stream, user_recursive_group_ids + ) or is_user_in_can_add_subscribers_group(stream, user_recursive_group_ids) def is_user_in_can_remove_subscribers_group( @@ -582,7 +596,8 @@ def user_has_content_access( ) # This check must be after the user_profile.is_guest check, since - # allow_everyone_group=False for can_add_subscribers_group. + # allow_everyone_group=False for can_add_subscribers_group and + # can_subscribe_group. if is_user_in_groups_granting_content_access( stream, user_group_membership_details.user_recursive_group_ids ): @@ -647,6 +662,7 @@ def has_metadata_access_to_channel_via_groups( user_recursive_group_ids: set[int], can_administer_channel_group_id: int, can_add_subscribers_group_id: int, + can_subscribe_group_id: int, ) -> bool: for setting_name in Stream.stream_permission_group_settings_granting_metadata_access: permission_configuration = Stream.stream_permission_group_settings[setting_name] @@ -659,6 +675,7 @@ def has_metadata_access_to_channel_via_groups( return ( can_administer_channel_group_id in user_recursive_group_ids or can_add_subscribers_group_id in user_recursive_group_ids + or can_subscribe_group_id in user_recursive_group_ids ) @@ -696,6 +713,7 @@ def check_basic_stream_access( user_group_membership_details.user_recursive_group_ids, stream.can_administer_channel_group_id, stream.can_add_subscribers_group_id, + stream.can_subscribe_group_id, ): return True @@ -926,7 +944,8 @@ def can_access_stream_metadata_user_ids(stream: Stream) -> set[int]: return public_stream_user_ids(stream) else: # for a private stream, it's subscribers plus channel admins - # and users belonging to `can_add_subscribers_group`. + # and users belonging to `can_add_subscribers_group` or + # `can_subscribe_group`. return ( private_stream_user_ids(stream.id) | {user.id for user in stream.realm.get_admin_users_and_bots()} diff --git a/zerver/lib/subscription_info.py b/zerver/lib/subscription_info.py index 0b0aa0a221..66d98818a3 100644 --- a/zerver/lib/subscription_info.py +++ b/zerver/lib/subscription_info.py @@ -358,6 +358,7 @@ def validate_user_access_to_subscribers(user_profile: UserProfile | None, stream "invite_only": stream.invite_only, "can_administer_channel_group_id": stream.can_administer_channel_group_id, "can_add_subscribers_group_id": stream.can_add_subscribers_group_id, + "can_subscribe_group_id": stream.can_subscribe_group_id, }, # We use a lambda here so that we only compute whether the # user is subscribed if we have to @@ -429,6 +430,7 @@ def validate_user_access_to_subscribers_helper( user_group_membership_details.user_recursive_group_ids, stream_dict["can_administer_channel_group_id"], stream_dict["can_add_subscribers_group_id"], + stream_dict["can_subscribe_group_id"], ): return @@ -595,6 +597,7 @@ def has_metadata_access_to_previously_subscribed_stream( user_recursive_group_ids: set[int], can_administer_channel_group_id: int, can_add_subscribers_group_id: int, + can_subscribe_group_id: int, ) -> bool: if stream_dict["is_web_public"]: return True @@ -608,6 +611,7 @@ def has_metadata_access_to_previously_subscribed_stream( user_recursive_group_ids, can_administer_channel_group_id, can_add_subscribers_group_id, + can_subscribe_group_id, ) return True @@ -714,12 +718,14 @@ def gather_subscriptions_helper( else: can_administer_channel_group_id = raw_stream_dict["can_administer_channel_group_id"] can_add_subscribers_group_id = raw_stream_dict["can_add_subscribers_group_id"] + can_subscribe_group_id = raw_stream_dict["can_subscribe_group_id"] if has_metadata_access_to_previously_subscribed_stream( user_profile, stream_dict, user_recursive_group_ids, can_administer_channel_group_id, can_add_subscribers_group_id, + can_subscribe_group_id, ): """ User who are no longer subscribed to a stream that they don't have @@ -748,11 +754,13 @@ def gather_subscriptions_helper( is_public = not raw_stream_dict["invite_only"] can_administer_channel_group_id = raw_stream_dict["can_administer_channel_group_id"] can_add_subscribers_group_id = raw_stream_dict["can_add_subscribers_group_id"] + can_subscribe_group_id = raw_stream_dict["can_subscribe_group_id"] has_metadata_access = has_metadata_access_to_channel_via_groups( user_profile, user_recursive_group_ids, can_administer_channel_group_id, can_add_subscribers_group_id, + can_subscribe_group_id, ) if is_public or user_profile.is_realm_admin or has_metadata_access: slim_stream_dict = build_stream_dict_for_never_sub( diff --git a/zerver/models/streams.py b/zerver/models/streams.py index 4c4c82d7a9..f212382e2c 100644 --- a/zerver/models/streams.py +++ b/zerver/models/streams.py @@ -187,6 +187,7 @@ class Stream(models.Model): stream_permission_group_settings_requiring_content_access = [ "can_add_subscribers_group", + "can_subscribe_group", ] assert set(stream_permission_group_settings_requiring_content_access).issubset( stream_permission_group_settings.keys() @@ -195,6 +196,7 @@ class Stream(models.Model): stream_permission_group_settings_granting_metadata_access = [ "can_add_subscribers_group", "can_administer_channel_group", + "can_subscribe_group", ] assert set(stream_permission_group_settings_granting_metadata_access).issubset( stream_permission_group_settings.keys() diff --git a/zerver/tests/test_subs.py b/zerver/tests/test_subs.py index 5c9a6ddf25..0002eae3e2 100644 --- a/zerver/tests/test_subs.py +++ b/zerver/tests/test_subs.py @@ -306,6 +306,9 @@ class TestCreateStreams(ZulipTestCase): prospero_group = check_add_user_group( realm, "prospero_group", [self.example_user("prospero")], acting_user=iago ) + cordelia_group = check_add_user_group( + realm, "cordelia_group", [self.example_user("cordelia")], acting_user=iago + ) with self.capture_send_event_calls(expected_num_events=1) as events: create_stream_if_needed( realm, @@ -313,18 +316,20 @@ class TestCreateStreams(ZulipTestCase): invite_only=True, can_administer_channel_group=aaron_group, can_add_subscribers_group=prospero_group, + can_subscribe_group=cordelia_group, ) self.assertEqual(events[0]["event"]["type"], "stream") self.assertEqual(events[0]["event"]["op"], "create") # Send private stream creation event to only realm admins. - self.assert_length(events[0]["users"], 4) + self.assert_length(events[0]["users"], 5) self.assertCountEqual( [ iago.id, self.example_user("desdemona").id, self.example_user("aaron").id, self.example_user("prospero").id, + self.example_user("cordelia").id, ], events[0]["users"], ) @@ -854,6 +859,7 @@ class TestCreateStreams(ZulipTestCase): self.assertEqual(stream.can_add_subscribers_group_id, nobody_group.id) self.assertEqual(stream.can_remove_subscribers_group_id, admins_group.id) self.assertEqual(stream.can_send_message_group_id, everyone_group.id) + self.assertEqual(stream.can_subscribe_group_id, nobody_group.id) # Check setting values sent in stream creation events. event_stream = events[0]["event"]["streams"][0] @@ -865,6 +871,7 @@ class TestCreateStreams(ZulipTestCase): self.assertEqual(event_stream["can_add_subscribers_group"], nobody_group.id) self.assertEqual(event_stream["can_remove_subscribers_group"], admins_group.id) self.assertEqual(event_stream["can_send_message_group"], everyone_group.id) + self.assertEqual(event_stream["can_subscribe_group"], nobody_group.id) def test_acting_user_is_creator(self) -> None: """ @@ -1881,6 +1888,7 @@ class StreamAdminTest(ZulipTestCase): cordelia = self.example_user("cordelia") aaron = self.example_user("aaron") prospero = self.example_user("prospero") + zoe = self.example_user("ZOE") realm = hamlet.realm stream = self.make_stream("private", invite_only=True) @@ -1909,6 +1917,13 @@ class StreamAdminTest(ZulipTestCase): prospero_group, acting_user=None, ) + zoe_group = check_add_user_group(realm, "zoe_group", [zoe], acting_user=hamlet) + do_change_stream_group_based_setting( + stream, + "can_subscribe_group", + zoe_group, + acting_user=None, + ) self.subscribe(self.example_user("cordelia"), "stream_private_name1") with self.capture_send_event_calls(expected_num_events=2) as events: do_unarchive_stream(stream, new_name="private", acting_user=None) @@ -1934,6 +1949,8 @@ class StreamAdminTest(ZulipTestCase): self.assertIn(self.example_user("aaron").id, notified_user_ids) # User belonging to `can_add_subscribers_group` should be notified. self.assertIn(prospero.id, notified_user_ids) + # User belonging to `can_subscribe_group` should be notified. + self.assertIn(zoe.id, notified_user_ids) # Guest user should not be notified. self.assertNotIn(self.example_user("polonius").id, notified_user_ids) @@ -2221,6 +2238,15 @@ class StreamAdminTest(ZulipTestCase): prospero_group, acting_user=None, ) + zoe_group = check_add_user_group( + realm, "zoe_group", [self.example_user("ZOE")], acting_user=user_profile + ) + do_change_stream_group_based_setting( + stream_private, + "can_subscribe_group", + zoe_group, + acting_user=None, + ) self.subscribe(self.example_user("cordelia"), "stream_private_name1") with self.capture_send_event_calls(expected_num_events=2) as events: stream_id = get_stream("stream_private_name1", realm).id @@ -2240,6 +2266,8 @@ class StreamAdminTest(ZulipTestCase): self.assertIn(self.example_user("aaron").id, notified_user_ids) # User belonging to `can_add_subscribers_group` should be notified. self.assertIn(self.example_user("prospero").id, notified_user_ids) + # User belonging to `can_subscribe_group` should be notified. + self.assertIn(self.example_user("ZOE").id, notified_user_ids) def test_rename_stream_requires_admin(self) -> None: user_profile = self.example_user("hamlet") @@ -5755,6 +5783,147 @@ class SubscriptionAPITest(ZulipTestCase): ) self.assert_json_error(result, "Unable to access channel (private_stream).") + def test_stream_settings_for_subscribing(self) -> None: + realm = get_realm("zulip") + + stream = self.make_stream("public_stream") + + nobody_group = NamedUserGroup.objects.get( + name=SystemGroups.NOBODY, realm=realm, is_system_group=True + ) + + def check_user_can_subscribe(user: UserProfile, error_msg: str | None = None) -> None: + result = self.subscribe_via_post( + user, + [stream.name], + allow_fail=error_msg is not None, + ) + if error_msg: + self.assert_json_error(result, error_msg) + return + + self.assertTrue( + Subscription.objects.filter( + recipient__type=Recipient.STREAM, + recipient__type_id=stream.id, + user_profile=user, + ).exists() + ) + # Unsubscribe user again for testing next case. + self.unsubscribe(user, stream.name) + + do_change_realm_permission_group_setting( + realm, "can_add_subscribers_group", nobody_group, acting_user=None + ) + do_change_stream_group_based_setting( + stream, "can_add_subscribers_group", nobody_group, acting_user=None + ) + do_change_stream_group_based_setting( + stream, "can_subscribe_group", nobody_group, acting_user=None + ) + + desdemona = self.example_user("desdemona") + shiva = self.example_user("shiva") + hamlet = self.example_user("hamlet") + polonius = self.example_user("polonius") + othello = self.example_user("othello") + check_user_can_subscribe(desdemona) + check_user_can_subscribe(shiva) + check_user_can_subscribe(hamlet) + check_user_can_subscribe(othello) + check_user_can_subscribe(polonius, "Not allowed for guest users") + + setting_group = self.create_or_update_anonymous_group_for_setting([polonius], []) + do_change_stream_group_based_setting( + stream, "can_subscribe_group", setting_group, acting_user=None + ) + + check_user_can_subscribe(polonius, "Not allowed for guest users") + + do_change_stream_group_based_setting( + stream, "can_subscribe_group", nobody_group, acting_user=None + ) + setting_group = self.create_or_update_anonymous_group_for_setting([polonius], []) + do_change_stream_group_based_setting( + stream, "can_add_subscribers_group", setting_group, acting_user=None + ) + + check_user_can_subscribe(polonius, "Not allowed for guest users") + + do_change_stream_group_based_setting( + stream, "can_add_subscribers_group", nobody_group, acting_user=None + ) + setting_group = self.create_or_update_anonymous_group_for_setting([polonius], []) + do_change_stream_group_based_setting( + stream, "can_administer_channel_group", setting_group, acting_user=None + ) + + check_user_can_subscribe(polonius, "Not allowed for guest users") + + stream = self.subscribe(self.example_user("iago"), "private_stream", invite_only=True) + + check_user_can_subscribe(desdemona, f"Unable to access channel ({stream.name}).") + check_user_can_subscribe(shiva, f"Unable to access channel ({stream.name}).") + check_user_can_subscribe(hamlet, f"Unable to access channel ({stream.name}).") + check_user_can_subscribe(othello, f"Unable to access channel ({stream.name}).") + + owners_group = NamedUserGroup.objects.get( + name=SystemGroups.OWNERS, realm=realm, is_system_group=True + ) + do_change_stream_group_based_setting( + stream, "can_subscribe_group", owners_group, acting_user=None + ) + + check_user_can_subscribe(shiva, f"Unable to access channel ({stream.name}).") + check_user_can_subscribe(hamlet, f"Unable to access channel ({stream.name}).") + check_user_can_subscribe(othello, f"Unable to access channel ({stream.name}).") + check_user_can_subscribe(desdemona) + + hamletcharacters_group = NamedUserGroup.objects.get(name="hamletcharacters", realm=realm) + do_change_stream_group_based_setting( + stream, "can_subscribe_group", hamletcharacters_group, acting_user=None + ) + check_user_can_subscribe(shiva, f"Unable to access channel ({stream.name}).") + check_user_can_subscribe(desdemona, f"Unable to access channel ({stream.name}).") + check_user_can_subscribe(othello, f"Unable to access channel ({stream.name}).") + check_user_can_subscribe(hamlet) + + setting_group = self.create_or_update_anonymous_group_for_setting([othello], [owners_group]) + do_change_stream_group_based_setting( + stream, "can_subscribe_group", setting_group, acting_user=None + ) + check_user_can_subscribe(shiva, f"Unable to access channel ({stream.name}).") + check_user_can_subscribe(hamlet, f"Unable to access channel ({stream.name}).") + check_user_can_subscribe(othello) + check_user_can_subscribe(desdemona) + + # Users can also subscribe if they are allowed to subscribe other users. + do_change_stream_group_based_setting( + stream, "can_subscribe_group", nobody_group, acting_user=None + ) + setting_group = self.create_or_update_anonymous_group_for_setting([othello], [owners_group]) + do_change_stream_group_based_setting( + stream, "can_add_subscribers_group", setting_group, acting_user=None + ) + check_user_can_subscribe(shiva, f"Unable to access channel ({stream.name}).") + check_user_can_subscribe(hamlet, f"Unable to access channel ({stream.name}).") + check_user_can_subscribe(othello) + check_user_can_subscribe(desdemona) + + # Users cannot subscribe if they belong to can_administer_channel_group but + # do not belong to any of can_subscribe_group and can_add_subscribers_group. + do_change_stream_group_based_setting( + stream, "can_add_subscribers_group", nobody_group, acting_user=None + ) + setting_group = self.create_or_update_anonymous_group_for_setting([othello], [owners_group]) + do_change_stream_group_based_setting( + stream, "can_administer_channel_group", setting_group, acting_user=None + ) + check_user_can_subscribe(shiva, f"Unable to access channel ({stream.name}).") + check_user_can_subscribe(hamlet, f"Unable to access channel ({stream.name}).") + check_user_can_subscribe(othello, f"Unable to access channel ({stream.name}).") + check_user_can_subscribe(desdemona, f"Unable to access channel ({stream.name}).") + def test_subscriptions_add_invalid_stream(self) -> None: """ Calling POST /json/users/me/subscriptions on a stream whose name is invalid (as @@ -6132,6 +6301,7 @@ class SubscriptionAPITest(ZulipTestCase): user5 = self.example_user("AARON") user6 = self.example_user("prospero") user7 = self.example_user("shiva") + user8 = self.example_user("ZOE") guest = self.example_user("polonius") realm = user1.realm @@ -6164,6 +6334,11 @@ class SubscriptionAPITest(ZulipTestCase): private, "can_add_subscribers_group", user7_and_guests_group, acting_user=user7 ) + user8_group = self.create_or_update_anonymous_group_for_setting([user8], []) + do_change_stream_group_based_setting( + private, "can_subscribe_group", user8_group, acting_user=user8 + ) + # Sends 3 peer-remove events, 2 unsubscribe events # and 2 stream delete events for private streams. with ( @@ -6195,6 +6370,7 @@ class SubscriptionAPITest(ZulipTestCase): user5.id, user6.id, user7.id, + user8.id, guest.id, } @@ -6211,19 +6387,24 @@ class SubscriptionAPITest(ZulipTestCase): self.assertEqual( notifications, [ - # user6 and user7 have metadata access to the channel - # via `can_administer_channel_group` and - # `can_add_subscribers_group` respectively. - ("private_stream", {user1.id, user2.id}, {user3.id, user4.id, user6.id, user7.id}), + # user6, user7 and user8 have metadata access to + # the channel via `can_administer_channel_group`, + # `can_add_subscribers_group` and `can_subscribe_group` + # respectively. + ( + "private_stream", + {user1.id, user2.id}, + {user3.id, user4.id, user6.id, user7.id, user8.id}, + ), ( "stream1", {user1.id, user2.id}, - {user3.id, user4.id, user5.id, user6.id, user7.id}, + {user3.id, user4.id, user5.id, user6.id, user7.id, user8.id}, ), ( "stream2,stream3", {user2.id}, - {user1.id, user3.id, user4.id, user5.id, user6.id, user7.id}, + {user1.id, user3.id, user4.id, user5.id, user6.id, user7.id, user8.id}, ), ], ) @@ -7173,24 +7354,40 @@ class InviteOnlyStreamTest(ZulipTestCase): self.assertEqual(json["subscribed"], {}) self.assertEqual(json["already_subscribed"], {}) - # Inviting another user to an invite-only stream is allowed - self.login_user(hamlet) - result = self.subscribe_via_post( - hamlet, - [stream_name], - extra_post_data={"principals": orjson.dumps([othello.id]).decode()}, + # Subscribing oneself to an invite-only stream is allowed + # if user belongs to can_subscribe_group. + stream = get_stream(stream_name, hamlet.realm) + setting_group = self.create_or_update_anonymous_group_for_setting([othello], []) + do_change_stream_group_based_setting( + stream, + "can_subscribe_group", + setting_group, + acting_user=None, ) + result = self.subscribe_via_post(othello, [stream_name]) json = self.assert_json_success(result) self.assertEqual(json["subscribed"], {str(othello.id): [stream_name]}) self.assertEqual(json["already_subscribed"], {}) - # Make sure both users are subscribed to this stream - stream_id = get_stream(stream_name, hamlet.realm).id - result = self.api_get(hamlet, f"/api/v1/streams/{stream_id}/members") + # Inviting another user to an invite-only stream is allowed + self.login_user(hamlet) + prospero = self.example_user("prospero") + result = self.subscribe_via_post( + hamlet, + [stream_name], + extra_post_data={"principals": orjson.dumps([prospero.id]).decode()}, + ) + json = self.assert_json_success(result) + self.assertEqual(json["subscribed"], {str(prospero.id): [stream_name]}) + self.assertEqual(json["already_subscribed"], {}) + + # Make sure all 3 users are subscribed to this stream + result = self.api_get(hamlet, f"/api/v1/streams/{stream.id}/members") json = self.assert_json_success(result) self.assertTrue(othello.id in json["subscribers"]) self.assertTrue(hamlet.id in json["subscribers"]) + self.assertTrue(prospero.id in json["subscribers"]) class GetSubscribersTest(ZulipTestCase): @@ -8130,6 +8327,36 @@ class AccessStreamTest(ZulipTestCase): with self.assertRaisesRegex(JsonableError, "Invalid channel name 'new_private_stream'"): access_stream_by_name(polonius, stream.name, require_content_access=True) + do_change_stream_group_based_setting( + stream, + "can_add_subscribers_group", + nobody_group, + acting_user=None, + ) + + do_change_stream_group_based_setting( + stream, + "can_subscribe_group", + polonius_and_othello_group, + acting_user=None, + ) + access_stream_by_id(othello, stream.id, require_content_access=False) + access_stream_by_name(othello, stream.name, require_content_access=False) + # Users in `can_subscribe_group` can access private + # stream if require_content_access is set to True + access_stream_by_id(othello, stream.id, require_content_access=True) + access_stream_by_name(othello, stream.name, require_content_access=True) + # Guest user who cannot access a stream via groups if they are + # part of `can_subscribe_group` but not subscribed to it. + with self.assertRaisesRegex(JsonableError, "Invalid channel ID"): + access_stream_by_id(polonius, stream.id, require_content_access=False) + with self.assertRaisesRegex(JsonableError, "Invalid channel name 'new_private_stream'"): + access_stream_by_name(polonius, stream.name, require_content_access=False) + with self.assertRaisesRegex(JsonableError, "Invalid channel ID"): + access_stream_by_id(polonius, stream.id, require_content_access=True) + with self.assertRaisesRegex(JsonableError, "Invalid channel name 'new_private_stream'"): + access_stream_by_name(polonius, stream.name, require_content_access=True) + def test_stream_access_by_guest(self) -> None: guest_user_profile = self.example_user("polonius") self.login_user(guest_user_profile) @@ -8295,6 +8522,37 @@ class AccessStreamTest(ZulipTestCase): acting_user=None, ) + # User should be able to access private channel if they are + # part of `can_subscribe_group` but not subscribed to the + # channel. + aaron_group = self.create_or_update_anonymous_group_for_setting([aaron], []) + do_change_stream_group_based_setting( + private_stream, + "can_subscribe_group", + aaron_group, + acting_user=None, + ) + self.assertEqual( + user_has_content_access( + aaron, + private_stream, + user_group_membership_details=UserGroupMembershipDetails( + user_recursive_group_ids=None + ), + is_subscribed=False, + ), + True, + ) + nobody_group = NamedUserGroup.objects.get( + name="role:nobody", realm=realm, is_system_group=True + ) + do_change_stream_group_based_setting( + private_stream, + "can_subscribe_group", + nobody_group, + acting_user=None, + ) + # User should not be able to access private channel if they are # part of `can_administer_channel_group` but not subscribed to # the channel.