mirror of https://github.com/zulip/zulip.git
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:
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
|
||||
|
|
Loading…
Reference in New Issue