mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-03 21:43:21 +00:00 
			
		
		
		
	stream_bots: Allow bot owners to unsubscribe their bots from streams.
Users who owns bots can unsubscribe their bots from streams. Fixes part of: #21402
This commit is contained in:
		
				
					committed by
					
						
						Tim Abbott
					
				
			
			
				
	
			
			
			
						parent
						
							45307affc0
						
					
				
				
					commit
					180a9cbdcb
				
			@@ -20,6 +20,11 @@ format used by the Zulip server that they are interacting with.
 | 
			
		||||
 | 
			
		||||
## Changes in Zulip 6.0
 | 
			
		||||
 | 
			
		||||
**Feature level 145**
 | 
			
		||||
 | 
			
		||||
* [`DELETE users/me/subscriptions`](/api/unsubscribe): Normal users can
 | 
			
		||||
  now remove bots that they own from streams.
 | 
			
		||||
 | 
			
		||||
**Feature level 144**
 | 
			
		||||
 | 
			
		||||
* [`GET /messages/{message_id}/read_receipts`](/api/get-read-receipts):
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.4.3"
 | 
			
		||||
# Changes should be accompanied by documentation explaining what the
 | 
			
		||||
# new level means in templates/zerver/api/changelog.md, as well as
 | 
			
		||||
# "**Changes**" entries in the endpoint's documentation in `zulip.yaml`.
 | 
			
		||||
API_FEATURE_LEVEL = 144
 | 
			
		||||
API_FEATURE_LEVEL = 145
 | 
			
		||||
 | 
			
		||||
# Bump the minor PROVISION_VERSION to indicate that folks should provision
 | 
			
		||||
# only when going from an old version of the code to a newer version. Bump
 | 
			
		||||
 
 | 
			
		||||
@@ -7736,6 +7736,16 @@ paths:
 | 
			
		||||
      tags: ["streams"]
 | 
			
		||||
      description: |
 | 
			
		||||
        Unsubscribe yourself or other users from one or more streams.
 | 
			
		||||
 | 
			
		||||
        In addition to managing the current user's subscriptions, this
 | 
			
		||||
        endpoint can be used by organization administrators to remove
 | 
			
		||||
        other users from streams, or to remove a bot that the current
 | 
			
		||||
        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 could remove other users from
 | 
			
		||||
        streams.
 | 
			
		||||
      x-curl-examples-parameters:
 | 
			
		||||
        oneOf:
 | 
			
		||||
          - type: include
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ from django.core.exceptions import ValidationError
 | 
			
		||||
from django.http import HttpResponse
 | 
			
		||||
from django.utils.timezone import now as timezone_now
 | 
			
		||||
 | 
			
		||||
from zerver.actions.bots import do_change_bot_owner
 | 
			
		||||
from zerver.actions.create_realm import do_create_realm
 | 
			
		||||
from zerver.actions.default_streams import (
 | 
			
		||||
    do_add_default_stream,
 | 
			
		||||
@@ -2236,12 +2237,12 @@ class StreamAdminTest(ZulipTestCase):
 | 
			
		||||
 | 
			
		||||
        return result
 | 
			
		||||
 | 
			
		||||
    def test_cant_remove_others_from_stream(self) -> None:
 | 
			
		||||
    def test_cant_remove_other_users_from_stream(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        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 except your own bots.
 | 
			
		||||
        """
 | 
			
		||||
        result = self.attempt_unsubscribe_of_principal(
 | 
			
		||||
            query_count=6,
 | 
			
		||||
            query_count=7,
 | 
			
		||||
            target_users=[self.example_user("cordelia")],
 | 
			
		||||
            is_realm_admin=False,
 | 
			
		||||
            is_subbed=True,
 | 
			
		||||
@@ -2279,10 +2280,11 @@ class StreamAdminTest(ZulipTestCase):
 | 
			
		||||
              using a queue.
 | 
			
		||||
        """
 | 
			
		||||
        target_users = [
 | 
			
		||||
            self.example_user(name) for name in ["cordelia", "prospero", "iago", "hamlet", "ZOE"]
 | 
			
		||||
            self.example_user(name)
 | 
			
		||||
            for name in ["cordelia", "prospero", "iago", "hamlet", "outgoing_webhook_bot"]
 | 
			
		||||
        ]
 | 
			
		||||
        result = self.attempt_unsubscribe_of_principal(
 | 
			
		||||
            query_count=26,
 | 
			
		||||
            query_count=27,
 | 
			
		||||
            cache_count=9,
 | 
			
		||||
            target_users=target_users,
 | 
			
		||||
            is_realm_admin=True,
 | 
			
		||||
@@ -2331,7 +2333,7 @@ class StreamAdminTest(ZulipTestCase):
 | 
			
		||||
 | 
			
		||||
    def test_cant_remove_others_from_stream_legacy_emails(self) -> None:
 | 
			
		||||
        result = self.attempt_unsubscribe_of_principal(
 | 
			
		||||
            query_count=6,
 | 
			
		||||
            query_count=7,
 | 
			
		||||
            is_realm_admin=False,
 | 
			
		||||
            is_subbed=True,
 | 
			
		||||
            invite_only=False,
 | 
			
		||||
@@ -2386,6 +2388,34 @@ class StreamAdminTest(ZulipTestCase):
 | 
			
		||||
        self.assert_length(json["removed"], 0)
 | 
			
		||||
        self.assert_length(json["not_removed"], 1)
 | 
			
		||||
 | 
			
		||||
    def test_bot_owner_can_remove_bot_from_stream(self) -> None:
 | 
			
		||||
        user_profile = self.example_user("hamlet")
 | 
			
		||||
        webhook_bot = self.example_user("webhook_bot")
 | 
			
		||||
        do_change_bot_owner(webhook_bot, bot_owner=user_profile, acting_user=user_profile)
 | 
			
		||||
        result = self.attempt_unsubscribe_of_principal(
 | 
			
		||||
            query_count=14,
 | 
			
		||||
            target_users=[webhook_bot],
 | 
			
		||||
            is_realm_admin=False,
 | 
			
		||||
            is_subbed=True,
 | 
			
		||||
            invite_only=False,
 | 
			
		||||
            target_users_subbed=True,
 | 
			
		||||
        )
 | 
			
		||||
        self.assert_json_success(result)
 | 
			
		||||
 | 
			
		||||
    def test_non_bot_owner_cannot_remove_bot_from_stream(self) -> None:
 | 
			
		||||
        other_user = self.example_user("cordelia")
 | 
			
		||||
        webhook_bot = self.example_user("webhook_bot")
 | 
			
		||||
        do_change_bot_owner(webhook_bot, bot_owner=other_user, acting_user=other_user)
 | 
			
		||||
        result = self.attempt_unsubscribe_of_principal(
 | 
			
		||||
            query_count=8,
 | 
			
		||||
            target_users=[webhook_bot],
 | 
			
		||||
            is_realm_admin=False,
 | 
			
		||||
            is_subbed=True,
 | 
			
		||||
            invite_only=False,
 | 
			
		||||
            target_users_subbed=True,
 | 
			
		||||
        )
 | 
			
		||||
        self.assert_json_error(result, "Insufficient permission")
 | 
			
		||||
 | 
			
		||||
    def test_can_remove_subscribers_group(self) -> None:
 | 
			
		||||
        realm = get_realm("zulip")
 | 
			
		||||
        leadership_group = create_user_group(
 | 
			
		||||
 
 | 
			
		||||
@@ -130,19 +130,14 @@ def principal_to_user_profile(agent: UserProfile, principal: Union[str, int]) ->
 | 
			
		||||
        raise PrincipalError(principal)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_if_removing_someone_else(
 | 
			
		||||
    user_profile: UserProfile, principals: Optional[Union[List[str], List[int]]]
 | 
			
		||||
) -> bool:
 | 
			
		||||
    if principals is None or len(principals) == 0:
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    if len(principals) > 1:
 | 
			
		||||
def user_directly_controls_user(user_profile: UserProfile, target: UserProfile) -> bool:
 | 
			
		||||
    """Returns whether the target user is either the current user or a bot
 | 
			
		||||
    owned by the current user"""
 | 
			
		||||
    if user_profile == target:
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    if isinstance(principals[0], int):
 | 
			
		||||
        return principals[0] != user_profile.id
 | 
			
		||||
    else:
 | 
			
		||||
        return principals[0] != user_profile.email
 | 
			
		||||
    if target.is_bot and target.bot_owner == user_profile:
 | 
			
		||||
        return True
 | 
			
		||||
    return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def deactivate_stream_backend(
 | 
			
		||||
@@ -464,23 +459,28 @@ def remove_subscriptions_backend(
 | 
			
		||||
) -> HttpResponse:
 | 
			
		||||
 | 
			
		||||
    realm = user_profile.realm
 | 
			
		||||
    removing_someone_else = check_if_removing_someone_else(user_profile, principals)
 | 
			
		||||
 | 
			
		||||
    streams_as_dict: List[StreamDict] = []
 | 
			
		||||
    for stream_name in streams_raw:
 | 
			
		||||
        streams_as_dict.append({"name": stream_name.strip()})
 | 
			
		||||
 | 
			
		||||
    streams, __ = list_to_streams(
 | 
			
		||||
        streams_as_dict, user_profile, unsubscribing_others=removing_someone_else
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    unsubscribing_others = False
 | 
			
		||||
    if principals:
 | 
			
		||||
        people_to_unsub = {
 | 
			
		||||
            principal_to_user_profile(user_profile, principal) for principal in principals
 | 
			
		||||
        }
 | 
			
		||||
        people_to_unsub = set()
 | 
			
		||||
        for principal in principals:
 | 
			
		||||
            target_user = principal_to_user_profile(user_profile, principal)
 | 
			
		||||
            people_to_unsub.add(target_user)
 | 
			
		||||
            if not user_directly_controls_user(user_profile, target_user):
 | 
			
		||||
                unsubscribing_others = True
 | 
			
		||||
    else:
 | 
			
		||||
        people_to_unsub = {user_profile}
 | 
			
		||||
 | 
			
		||||
    streams, __ = list_to_streams(
 | 
			
		||||
        streams_as_dict,
 | 
			
		||||
        user_profile,
 | 
			
		||||
        unsubscribing_others=unsubscribing_others,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    result: Dict[str, List[str]] = dict(removed=[], not_removed=[])
 | 
			
		||||
    (removed, not_subscribed) = bulk_remove_subscriptions(
 | 
			
		||||
        realm, people_to_unsub, streams, acting_user=user_profile
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user