mirror of
https://github.com/zulip/zulip.git
synced 2025-11-04 14:03:30 +00:00
stream: Allow realm & channel admins to change private channel setting.
Previously, realm and channel admins were not able to change settings for a private channel they were not subscribed to. This commit changes that. We have only added the exception for can_add_subscribers_group and not privacy settings. We also need proper functions with proper terminologies for content and metadata access.
This commit is contained in:
committed by
Tim Abbott
parent
4d02a082a0
commit
ca1aba9fc3
@@ -29,6 +29,12 @@ format used by the Zulip server that they are interacting with.
|
||||
administrators can now unsubscribe other users even if they are not
|
||||
an organization administrator or part of
|
||||
`can_remove_subscribers_group`.
|
||||
* [`PATCH /streams/{stream_id}`](/api/update-stream),
|
||||
[`DELETE /streams/{stream_id}`](/api/archive-stream): Channel and
|
||||
organization administrators can modify all the settings requiring
|
||||
only metadata access without having content access to it. They
|
||||
cannot add subscribers to the channel or change it's privacy setting
|
||||
without having content access to it.
|
||||
|
||||
**Feature level 348**
|
||||
|
||||
|
||||
@@ -447,7 +447,32 @@ def check_for_exactly_one_stream_arg(stream_id: int | None, stream: str | None)
|
||||
raise IncompatibleParametersError(["stream_id", "stream"])
|
||||
|
||||
|
||||
def check_stream_access_for_delete_or_update(
|
||||
def user_has_content_access(
|
||||
user_profile: UserProfile,
|
||||
stream: Stream,
|
||||
*,
|
||||
is_subscribed: bool,
|
||||
) -> bool:
|
||||
if stream.is_web_public:
|
||||
return True
|
||||
|
||||
if is_subscribed:
|
||||
return True
|
||||
|
||||
if not stream.invite_only and not user_profile.is_guest:
|
||||
return True
|
||||
|
||||
user_recursive_group_ids = set(
|
||||
get_recursive_membership_groups(user_profile).values_list("id", flat=True)
|
||||
)
|
||||
|
||||
if is_user_in_can_add_subscribers_group(stream, user_recursive_group_ids):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def check_stream_access_for_delete_or_update_requiring_metadata_access(
|
||||
user_profile: UserProfile, stream: Stream, sub: Subscription | None = None
|
||||
) -> None:
|
||||
error = _("Invalid channel ID")
|
||||
@@ -461,16 +486,22 @@ def check_stream_access_for_delete_or_update(
|
||||
if user_profile.is_realm_admin:
|
||||
return
|
||||
|
||||
if sub is None and stream.invite_only:
|
||||
raise JsonableError(error)
|
||||
|
||||
if can_administer_accessible_channel(stream, user_profile):
|
||||
return
|
||||
|
||||
# We only want to reveal that user is not an administrator
|
||||
# if the user has access to the channel in the first place.
|
||||
# Ideally, we would be checking if user has metadata access
|
||||
# to the channel for this block, but since we have ruled out
|
||||
# the possibility that the user is a channel admin, checking
|
||||
# for content access will save us valuable DB queries.
|
||||
if user_has_content_access(user_profile, stream, is_subscribed=sub is not None):
|
||||
raise CannotAdministerChannelError
|
||||
|
||||
raise JsonableError(error)
|
||||
|
||||
def access_stream_for_delete_or_update(
|
||||
|
||||
def access_stream_for_delete_or_update_requiring_metadata_access(
|
||||
user_profile: UserProfile, stream_id: int
|
||||
) -> tuple[Stream, Subscription | None]:
|
||||
try:
|
||||
@@ -485,7 +516,7 @@ def access_stream_for_delete_or_update(
|
||||
except Subscription.DoesNotExist:
|
||||
sub = None
|
||||
|
||||
check_stream_access_for_delete_or_update(user_profile, stream, sub)
|
||||
check_stream_access_for_delete_or_update_requiring_metadata_access(user_profile, stream, sub)
|
||||
return (stream, sub)
|
||||
|
||||
|
||||
|
||||
@@ -177,6 +177,13 @@ class Stream(models.Model):
|
||||
),
|
||||
}
|
||||
|
||||
stream_permission_group_settings_requiring_content_access = [
|
||||
"can_add_subscribers_group",
|
||||
]
|
||||
assert set(stream_permission_group_settings_requiring_content_access).issubset(
|
||||
stream_permission_group_settings.keys()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(Upper("name"), name="upper_stream_name_idx"),
|
||||
|
||||
@@ -20486,12 +20486,16 @@ paths:
|
||||
|
||||
Administrators can always administer a channel.
|
||||
|
||||
Note that a user must also [have access](/help/channel-permissions) to a
|
||||
channel in order to modify it. The exception to this rule is that
|
||||
organization administrators can edit channel names and descriptions
|
||||
without having full access to the channel.
|
||||
Note that a user must [have access](/help/channel-permissions) to a
|
||||
channel in order to add other subscribers to the channel.
|
||||
|
||||
**Changes**: New in Zulip 10.0 (feature level 325). Prior to this
|
||||
**Changes**: Prior to Zulip 10.0 (feature level 349) a user needed to
|
||||
[have content access](/help/channel-permissions) to a channel in order
|
||||
to modify it. The exception to this rule was that organization
|
||||
administrators can edit channel names and descriptions without having
|
||||
full access to the channel.
|
||||
|
||||
New in Zulip 10.0 (feature level 325). Prior to this
|
||||
change, the permission to administer channels was limited to realm
|
||||
administrators.
|
||||
example:
|
||||
@@ -25195,16 +25199,16 @@ components:
|
||||
|
||||
Administrators can always administer a channel.
|
||||
|
||||
Note that a user must also [have access](/help/channel-permissions) to a
|
||||
channel in order to modify it.
|
||||
Note that a user must [have access](/help/channel-permissions) to a
|
||||
channel in order to add other subscribers to the channel.
|
||||
|
||||
Users can edit channel name and description without subscribing to the
|
||||
channel, but they need to be subscribed to edit channel permissions and
|
||||
add users. The exception to this rule is that organization administrators
|
||||
can edit channel names and descriptions without having full access to
|
||||
the channel.
|
||||
**Changes**: Prior to Zulip 10.0 (feature level 349) a user needed to
|
||||
[have content access](/help/channel-permissions) to a channel in
|
||||
order to modify it. The exception to this rule was that organization
|
||||
administrators can edit channel names and descriptions without
|
||||
having full access to the channel.
|
||||
|
||||
**Changes**: New in Zulip 10.0 (feature level 325). Prior to this
|
||||
New in Zulip 10.0 (feature level 325). Prior to this
|
||||
change, the permission to administer channels was limited to realm
|
||||
administrators.
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ from zerver.lib.streams import (
|
||||
ensure_stream,
|
||||
filter_stream_authorization,
|
||||
list_to_streams,
|
||||
user_has_content_access,
|
||||
)
|
||||
from zerver.lib.subscription_info import (
|
||||
bulk_get_subscriber_user_ids,
|
||||
@@ -2125,12 +2126,18 @@ class StreamAdminTest(ZulipTestCase):
|
||||
def test_non_admin_cannot_access_unsub_private_stream(self) -> None:
|
||||
iago = self.example_user("iago")
|
||||
hamlet = self.example_user("hamlet")
|
||||
nobody_group = NamedUserGroup.objects.get(
|
||||
name="role:nobody", is_system_group=True, realm=hamlet.realm
|
||||
)
|
||||
|
||||
self.login_user(hamlet)
|
||||
result = self.subscribe_via_post(
|
||||
hamlet,
|
||||
["private_stream_1"],
|
||||
dict(principals=orjson.dumps([iago.id]).decode()),
|
||||
dict(
|
||||
principals=orjson.dumps([iago.id]).decode(),
|
||||
can_administer_channel_group=nobody_group.id,
|
||||
),
|
||||
invite_only=True,
|
||||
)
|
||||
self.assert_json_success(result)
|
||||
@@ -2669,26 +2676,81 @@ class StreamAdminTest(ZulipTestCase):
|
||||
f"'{setting_name}' setting cannot be set to 'role:internet' group.",
|
||||
)
|
||||
|
||||
# For private streams, even admins must be subscribed to the
|
||||
# stream to change the setting.
|
||||
# For private streams, realm admins need not be subscribed to
|
||||
# the stream to change the setting as they can administer the
|
||||
# channel by default.
|
||||
stream = get_stream("stream_name2", realm)
|
||||
params[setting_name] = orjson.dumps({"new": moderators_system_group.id}).decode()
|
||||
result = self.client_patch(
|
||||
f"/json/streams/{stream.id}",
|
||||
params,
|
||||
)
|
||||
if setting_name in Stream.stream_permission_group_settings_requiring_content_access:
|
||||
self.assert_json_error(result, "Invalid channel ID")
|
||||
else:
|
||||
self.assert_json_success(result)
|
||||
stream = get_stream("stream_name2", realm)
|
||||
self.assertEqual(getattr(stream, setting_name).id, moderators_system_group.id)
|
||||
|
||||
self.subscribe(user_profile, "stream_name2")
|
||||
# For private streams, channel admins need not be subscribed to
|
||||
# the stream to change the setting as they can administer the
|
||||
# channel by default.
|
||||
shiva_group = self.create_or_update_anonymous_group_for_setting([shiva], [])
|
||||
do_change_stream_group_based_setting(
|
||||
stream,
|
||||
"can_administer_channel_group",
|
||||
shiva_group,
|
||||
acting_user=None,
|
||||
)
|
||||
self.assertTrue(is_user_in_group(stream.can_administer_channel_group, shiva))
|
||||
params[setting_name] = orjson.dumps({"new": owners_group.id}).decode()
|
||||
self.login_user(shiva)
|
||||
result = self.client_patch(
|
||||
f"/json/streams/{stream.id}",
|
||||
params,
|
||||
)
|
||||
if setting_name in Stream.stream_permission_group_settings_requiring_content_access:
|
||||
self.assert_json_error(result, "Invalid channel ID")
|
||||
shiva_group = self.create_or_update_anonymous_group_for_setting([shiva], [])
|
||||
do_change_stream_group_based_setting(
|
||||
stream,
|
||||
"can_add_subscribers_group",
|
||||
shiva_group,
|
||||
acting_user=None,
|
||||
)
|
||||
result = self.client_patch(
|
||||
f"/json/streams/{stream.id}",
|
||||
params,
|
||||
)
|
||||
self.assert_json_success(result)
|
||||
stream = get_stream("stream_name2", realm)
|
||||
self.assertEqual(getattr(stream, setting_name).id, moderators_system_group.id)
|
||||
# Unsubscribe user from private stream to test next setting.
|
||||
self.unsubscribe(user_profile, "stream_name2")
|
||||
self.assertEqual(getattr(stream, setting_name).id, owners_group.id)
|
||||
else:
|
||||
self.assert_json_success(result)
|
||||
stream = get_stream("stream_name2", realm)
|
||||
self.assertEqual(getattr(stream, setting_name).id, owners_group.id)
|
||||
|
||||
# Guest user cannot be a channel admin for a public channel.
|
||||
# `user_has_permission_for_group_setting` will not allow a guest
|
||||
# to be a part of `can_administer_channel_group` since that
|
||||
# group has `allow_everyone_group` set to false.
|
||||
stream = get_stream("stream_name1", realm)
|
||||
polonius = self.example_user("polonius")
|
||||
polonius_group = self.create_or_update_anonymous_group_for_setting([polonius], [])
|
||||
do_change_stream_group_based_setting(
|
||||
stream,
|
||||
"can_administer_channel_group",
|
||||
polonius_group,
|
||||
acting_user=None,
|
||||
)
|
||||
subbed_users = self.users_subscribed_to_stream(stream.name, polonius.realm)
|
||||
self.assertNotIn(polonius, subbed_users)
|
||||
self.login_user(polonius)
|
||||
result = self.client_patch(
|
||||
f"/json/streams/{stream.id}",
|
||||
params,
|
||||
)
|
||||
self.assert_json_error(result, "Invalid channel ID")
|
||||
|
||||
def test_changing_stream_permission_settings(self) -> None:
|
||||
self.make_stream("stream_name1")
|
||||
@@ -7602,6 +7664,72 @@ class AccessStreamTest(ZulipTestCase):
|
||||
assert sub_ret is None
|
||||
self.assertEqual(stream.id, stream_ret.id)
|
||||
|
||||
def test_has_content_access(self) -> None:
|
||||
guest_user = self.example_user("polonius")
|
||||
aaron = self.example_user("aaron")
|
||||
realm = guest_user.realm
|
||||
web_public_stream = self.make_stream("web_public_stream", realm=realm, is_web_public=True)
|
||||
private_stream = self.make_stream("private_stream", realm=realm, invite_only=True)
|
||||
public_stream = self.make_stream("public_stream", realm=realm, invite_only=False)
|
||||
|
||||
# Even guest user should have access to web public channel.
|
||||
self.assertEqual(
|
||||
user_has_content_access(guest_user, web_public_stream, is_subscribed=False), True
|
||||
)
|
||||
|
||||
# User should have access to private channel if they are
|
||||
# subscribed to it
|
||||
self.assertEqual(user_has_content_access(aaron, private_stream, is_subscribed=True), True)
|
||||
self.assertEqual(user_has_content_access(aaron, private_stream, is_subscribed=False), False)
|
||||
|
||||
# Non guest user should have access to public channel
|
||||
# regardless of their subscription to the channel.
|
||||
self.assertEqual(user_has_content_access(aaron, public_stream, is_subscribed=True), True)
|
||||
self.assertEqual(user_has_content_access(aaron, public_stream, is_subscribed=False), True)
|
||||
|
||||
# Guest user should have access to public channel only if they
|
||||
# are subscribed to it.
|
||||
self.assertEqual(
|
||||
user_has_content_access(guest_user, public_stream, is_subscribed=False), False
|
||||
)
|
||||
self.assertEqual(
|
||||
user_has_content_access(guest_user, public_stream, is_subscribed=True), True
|
||||
)
|
||||
|
||||
# User should be able to access private channel if they are
|
||||
# part of `can_add_subscribers_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_add_subscribers_group",
|
||||
aaron_group,
|
||||
acting_user=None,
|
||||
)
|
||||
self.assertEqual(user_has_content_access(aaron, private_stream, 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_add_subscribers_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.
|
||||
aaron_group = self.create_or_update_anonymous_group_for_setting([aaron], [])
|
||||
do_change_stream_group_based_setting(
|
||||
private_stream,
|
||||
"can_administer_channel_group",
|
||||
aaron_group,
|
||||
acting_user=None,
|
||||
)
|
||||
self.assertEqual(user_has_content_access(aaron, private_stream, is_subscribed=False), False)
|
||||
self.assertEqual(user_has_content_access(aaron, private_stream, is_subscribed=True), True)
|
||||
|
||||
|
||||
class StreamTrafficTest(ZulipTestCase):
|
||||
def test_average_weekly_stream_traffic_calculation(self) -> None:
|
||||
|
||||
@@ -65,7 +65,7 @@ from zerver.lib.streams import (
|
||||
access_default_stream_group_by_id,
|
||||
access_stream_by_id,
|
||||
access_stream_by_name,
|
||||
access_stream_for_delete_or_update,
|
||||
access_stream_for_delete_or_update_requiring_metadata_access,
|
||||
access_web_public_stream,
|
||||
check_stream_name_available,
|
||||
do_get_streams,
|
||||
@@ -75,6 +75,7 @@ from zerver.lib.streams import (
|
||||
get_stream_permission_policy_name,
|
||||
list_to_streams,
|
||||
stream_to_dict,
|
||||
user_has_content_access,
|
||||
)
|
||||
from zerver.lib.subscription_info import gather_subscriptions
|
||||
from zerver.lib.topic import (
|
||||
@@ -142,7 +143,9 @@ def user_directly_controls_user(user_profile: UserProfile, target: UserProfile)
|
||||
def deactivate_stream_backend(
|
||||
request: HttpRequest, user_profile: UserProfile, stream_id: int
|
||||
) -> HttpResponse:
|
||||
(stream, sub) = access_stream_for_delete_or_update(user_profile, stream_id)
|
||||
(stream, sub) = access_stream_for_delete_or_update_requiring_metadata_access(
|
||||
user_profile, stream_id
|
||||
)
|
||||
do_deactivate_stream(stream, acting_user=user_profile)
|
||||
return json_success(request)
|
||||
|
||||
@@ -266,9 +269,12 @@ def update_stream_backend(
|
||||
can_send_message_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
can_remove_subscribers_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
) -> HttpResponse:
|
||||
# We allow realm administrators to update the stream name and
|
||||
# description even for private streams.
|
||||
(stream, sub) = access_stream_for_delete_or_update(user_profile, stream_id)
|
||||
# Most settings updates only require metadata access, not content
|
||||
# access. We will check for content access further when and where
|
||||
# required.
|
||||
(stream, sub) = access_stream_for_delete_or_update_requiring_metadata_access(
|
||||
user_profile, stream_id
|
||||
)
|
||||
|
||||
# Validate that the proposed state for permissions settings is permitted.
|
||||
if is_private is not None:
|
||||
@@ -325,7 +331,7 @@ def update_stream_backend(
|
||||
raise JsonableError(_("Moderation request channel must be private."))
|
||||
|
||||
if is_private is not None:
|
||||
# We require even realm administrators to be actually
|
||||
# We require even channel administrators to be actually
|
||||
# subscribed to make a private stream public, via this
|
||||
# stricted access_stream check.
|
||||
access_stream_by_id(user_profile, stream_id)
|
||||
@@ -405,8 +411,10 @@ def update_stream_backend(
|
||||
if validate_group_setting_value_change(
|
||||
current_setting_api_value, new_setting_value, expected_current_setting_value
|
||||
):
|
||||
if sub is None and stream.invite_only:
|
||||
# Admins cannot change this setting for unsubscribed private streams.
|
||||
if (
|
||||
setting_name in Stream.stream_permission_group_settings_requiring_content_access
|
||||
and not user_has_content_access(user_profile, stream, is_subscribed=sub is not None)
|
||||
):
|
||||
raise JsonableError(_("Invalid channel ID"))
|
||||
with transaction.atomic(durable=True):
|
||||
user_group = access_user_group_for_setting(
|
||||
|
||||
Reference in New Issue
Block a user