mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	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:
		@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user