streams: Allow changing can_remove_subscribers_group through API.

This commit adds API support to change can_remove_subscribers_group
setting for a stream.
This commit is contained in:
Sahil Batra 2022-09-16 17:57:38 +05:30 committed by Tim Abbott
parent 6fa0a83d6b
commit c3759814be
5 changed files with 157 additions and 7 deletions

View File

@ -20,6 +20,12 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 7.0 ## Changes in Zulip 7.0
**Feature level 161**
* [`PATCH /streams/{stream_id}`](/api/update-stream): Added
`can_remove_subscribers_group_id` parameter to support
changing `can_remove_subscribers_group` setting.
**Feature level 160** **Feature level 160**
* [`POST /api/v1/jwt/fetch_api_key`]: New API endpoint to fetch API * [`POST /api/v1/jwt/fetch_api_key`]: New API endpoint to fetch API

View File

@ -58,6 +58,33 @@ def access_user_groups_as_potential_subgroups(
return list(user_groups) return list(user_groups)
def access_user_group_for_setting(
user_group_id: int,
user_profile: UserProfile,
*,
setting_name: str,
require_system_group: bool = False,
allow_internet_group: bool = False,
allow_owners_group: bool = False,
) -> UserGroup:
user_group = access_user_group_by_id(user_group_id, user_profile, for_read=True)
if require_system_group and not user_group.is_system_group:
raise JsonableError(_("'{}' must be a system user group.").format(setting_name))
if not allow_internet_group and user_group.name == UserGroup.EVERYONE_ON_INTERNET_GROUP_NAME:
raise JsonableError(
_("'{}' setting cannot be set to '@role:internet' group.").format(setting_name)
)
if not allow_owners_group and user_group.name == UserGroup.OWNERS_GROUP_NAME:
raise JsonableError(
_("'{}' setting cannot be set to '@role:owners' group.").format(setting_name)
)
return user_group
def user_groups_in_realm_serialized(realm: Realm) -> List[UserGroupDict]: def user_groups_in_realm_serialized(realm: Realm) -> List[UserGroupDict]:
"""This function is used in do_events_register code path so this code """This function is used in do_events_register code path so this code
should be performant. We need to do 2 database queries because should be performant. We need to do 2 database queries because

View File

@ -8027,14 +8027,23 @@ paths:
Unsubscribe yourself or other users from one or more streams. Unsubscribe yourself or other users from one or more streams.
In addition to managing the current user's subscriptions, this In addition to managing the current user's subscriptions, this
endpoint can be used by organization administrators to remove endpoint can be used to remove other users from streams. This
other users from streams, or to remove a bot that the current is possible in 3 situations:
user is the `bot_owner` for from any stream that the current
user can access.
**Changes**: Before Zulip 6.0 (feature level 145), only - Organization administrators can remove any user from any
organization administrators could remove other users from stream.
streams. - Users can remove a bot that they own from any stream that
the user can access.
- Users who can access a stream and are in the group with ID
`can_remove_subscribers_group_id` for that stream can
unsubscribe any user from that stream.
**Changes**: Before Zulip 7.0 (feature level 161),
`can_remove_subscribers_group_id` was always the system group
for organization administrators.
Before Zulip 6.0 (feature level 145), users had no special
privileges for managing bots that they own.
x-curl-examples-parameters: x-curl-examples-parameters:
oneOf: oneOf:
- type: include - type: include
@ -14536,6 +14545,7 @@ paths:
required: false required: false
- $ref: "#/components/parameters/StreamPostPolicy" - $ref: "#/components/parameters/StreamPostPolicy"
- $ref: "#/components/parameters/MessageRetentionDays" - $ref: "#/components/parameters/MessageRetentionDays"
- $ref: "#/components/parameters/CanRemoveSubscribersGroupId"
responses: responses:
"200": "200":
$ref: "#/components/responses/SimpleSuccess" $ref: "#/components/responses/SimpleSuccess"
@ -17562,6 +17572,22 @@ components:
- type: integer - type: integer
example: "20" example: "20"
required: false required: false
CanRemoveSubscribersGroupId:
name: can_remove_subscribers_group_id
in: query
description: |
ID of the user group whose members are allowed to unsubscribe others
from the stream, if they have access to this stream, even if
they are not an organization administrator.
This setting can currently only be set to system user groups
except `@role:internet` and `@role:owners` group.
**Changes**: New in Zulip 7.0 (feature level 161).
schema:
type: integer
example: 20
required: false
LinkifierPattern: LinkifierPattern:
name: pattern name: pattern
in: query in: query

View File

@ -1977,6 +1977,80 @@ class StreamAdminTest(ZulipTestCase):
stream = get_stream("stream_name1", realm) stream = get_stream("stream_name1", realm)
self.assertEqual(stream.message_retention_days, 2) self.assertEqual(stream.message_retention_days, 2)
def test_change_stream_can_remove_subscribers_group(self) -> None:
user_profile = self.example_user("iago")
realm = user_profile.realm
stream = self.subscribe(user_profile, "stream_name1")
moderators_system_group = UserGroup.objects.get(
name="@role:moderators", realm=realm, is_system_group=True
)
self.login("shiva")
result = self.client_patch(
f"/json/streams/{stream.id}",
{"can_remove_subscribers_group_id": orjson.dumps(moderators_system_group.id).decode()},
)
self.assert_json_error(result, "Must be an organization administrator")
self.login("iago")
result = self.client_patch(
f"/json/streams/{stream.id}",
{"can_remove_subscribers_group_id": orjson.dumps(moderators_system_group.id).decode()},
)
self.assert_json_success(result)
stream = get_stream("stream_name1", realm)
self.assertEqual(stream.can_remove_subscribers_group.id, moderators_system_group.id)
# This setting can only be set to system groups.
hamletcharacters_group = UserGroup.objects.get(name="hamletcharacters", realm=realm)
result = self.client_patch(
f"/json/streams/{stream.id}",
{"can_remove_subscribers_group_id": orjson.dumps(hamletcharacters_group.id).decode()},
)
self.assert_json_error(
result, "'can_remove_subscribers_group' must be a system user group."
)
internet_group = UserGroup.objects.get(
name="@role:internet", is_system_group=True, realm=realm
)
result = self.client_patch(
f"/json/streams/{stream.id}",
{"can_remove_subscribers_group_id": orjson.dumps(internet_group.id).decode()},
)
self.assert_json_error(
result,
"'can_remove_subscribers_group' setting cannot be set to '@role:internet' group.",
)
owners_group = UserGroup.objects.get(name="@role:owners", is_system_group=True, realm=realm)
result = self.client_patch(
f"/json/streams/{stream.id}",
{"can_remove_subscribers_group_id": orjson.dumps(owners_group.id).decode()},
)
self.assert_json_error(
result,
"'can_remove_subscribers_group' setting cannot be set to '@role:owners' group.",
)
# For private streams, even admins must be subscribed to the stream to change
# can_remove_subscribers_group setting.
stream = self.make_stream("stream_name2", invite_only=True)
result = self.client_patch(
f"/json/streams/{stream.id}",
{"can_remove_subscribers_group_id": orjson.dumps(moderators_system_group.id).decode()},
)
self.assert_json_error(result, "Invalid stream ID")
self.subscribe(user_profile, "stream_name2")
result = self.client_patch(
f"/json/streams/{stream.id}",
{"can_remove_subscribers_group_id": orjson.dumps(moderators_system_group.id).decode()},
)
self.assert_json_success(result)
stream = get_stream("stream_name2", realm)
self.assertEqual(stream.can_remove_subscribers_group.id, moderators_system_group.id)
def test_stream_message_retention_days_on_stream_creation(self) -> None: def test_stream_message_retention_days_on_stream_creation(self) -> None:
""" """
Only admins can create streams with message_retention_days Only admins can create streams with message_retention_days

View File

@ -30,6 +30,7 @@ from zerver.actions.message_send import (
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_description, do_change_stream_description,
do_change_stream_message_retention_days, do_change_stream_message_retention_days,
do_change_stream_permission, do_change_stream_permission,
@ -79,6 +80,7 @@ from zerver.lib.topic import (
messages_for_topic, messages_for_topic,
) )
from zerver.lib.types import Validator from zerver.lib.types import Validator
from zerver.lib.user_groups import access_user_group_for_setting
from zerver.lib.utils import assert_is_not_none from zerver.lib.utils import assert_is_not_none
from zerver.lib.validator import ( from zerver.lib.validator import (
check_bool, check_bool,
@ -266,6 +268,7 @@ def update_stream_backend(
message_retention_days: Optional[Union[int, str]] = REQ( message_retention_days: Optional[Union[int, str]] = REQ(
json_validator=check_string_or_int, default=None json_validator=check_string_or_int, default=None
), ),
can_remove_subscribers_group_id: Optional[int] = REQ(json_validator=check_int, default=None),
) -> HttpResponse: ) -> HttpResponse:
# We allow realm administrators to to update the stream name and # We allow realm administrators to to update the stream name and
# description even for private streams. # description even for private streams.
@ -380,6 +383,20 @@ def update_stream_backend(
if stream_post_policy is not None: if stream_post_policy is not None:
do_change_stream_post_policy(stream, stream_post_policy, acting_user=user_profile) do_change_stream_post_policy(stream, stream_post_policy, acting_user=user_profile)
if can_remove_subscribers_group_id is not None:
if sub is None and stream.invite_only:
# Admins cannot change this setting for unsubscribed private streams.
raise JsonableError(_("Invalid stream ID"))
user_group = access_user_group_for_setting(
can_remove_subscribers_group_id,
user_profile,
setting_name="can_remove_subscribers_group",
require_system_group=True,
)
do_change_can_remove_subscribers_group(stream, user_group, acting_user=user_profile)
return json_success(request) return json_success(request)