mirror of
https://github.com/zulip/zulip.git
synced 2025-11-09 16:37:23 +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
|
administrators can now unsubscribe other users even if they are not
|
||||||
an organization administrator or part of
|
an organization administrator or part of
|
||||||
`can_remove_subscribers_group`.
|
`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**
|
**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"])
|
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
|
user_profile: UserProfile, stream: Stream, sub: Subscription | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
error = _("Invalid channel ID")
|
error = _("Invalid channel ID")
|
||||||
@@ -461,16 +486,22 @@ def check_stream_access_for_delete_or_update(
|
|||||||
if user_profile.is_realm_admin:
|
if user_profile.is_realm_admin:
|
||||||
return
|
return
|
||||||
|
|
||||||
if sub is None and stream.invite_only:
|
|
||||||
raise JsonableError(error)
|
|
||||||
|
|
||||||
if can_administer_accessible_channel(stream, user_profile):
|
if can_administer_accessible_channel(stream, user_profile):
|
||||||
return
|
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 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
|
user_profile: UserProfile, stream_id: int
|
||||||
) -> tuple[Stream, Subscription | None]:
|
) -> tuple[Stream, Subscription | None]:
|
||||||
try:
|
try:
|
||||||
@@ -485,7 +516,7 @@ def access_stream_for_delete_or_update(
|
|||||||
except Subscription.DoesNotExist:
|
except Subscription.DoesNotExist:
|
||||||
sub = None
|
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)
|
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:
|
class Meta:
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(Upper("name"), name="upper_stream_name_idx"),
|
models.Index(Upper("name"), name="upper_stream_name_idx"),
|
||||||
|
|||||||
@@ -20486,12 +20486,16 @@ paths:
|
|||||||
|
|
||||||
Administrators can always administer a channel.
|
Administrators can always administer a channel.
|
||||||
|
|
||||||
Note that a user must also [have access](/help/channel-permissions) to a
|
Note that a user must [have access](/help/channel-permissions) to a
|
||||||
channel in order to modify it. The exception to this rule is that
|
channel in order to add other subscribers to the channel.
|
||||||
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
|
**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
|
change, the permission to administer channels was limited to realm
|
||||||
administrators.
|
administrators.
|
||||||
example:
|
example:
|
||||||
@@ -25195,16 +25199,16 @@ components:
|
|||||||
|
|
||||||
Administrators can always administer a channel.
|
Administrators can always administer a channel.
|
||||||
|
|
||||||
Note that a user must also [have access](/help/channel-permissions) to a
|
Note that a user must [have access](/help/channel-permissions) to a
|
||||||
channel in order to modify it.
|
channel in order to add other subscribers to the channel.
|
||||||
|
|
||||||
Users can edit channel name and description without subscribing to the
|
**Changes**: Prior to Zulip 10.0 (feature level 349) a user needed to
|
||||||
channel, but they need to be subscribed to edit channel permissions and
|
[have content access](/help/channel-permissions) to a channel in
|
||||||
add users. The exception to this rule is that organization administrators
|
order to modify it. The exception to this rule was that organization
|
||||||
can edit channel names and descriptions without having full access to
|
administrators can edit channel names and descriptions without
|
||||||
the channel.
|
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
|
change, the permission to administer channels was limited to realm
|
||||||
administrators.
|
administrators.
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ from zerver.lib.streams import (
|
|||||||
ensure_stream,
|
ensure_stream,
|
||||||
filter_stream_authorization,
|
filter_stream_authorization,
|
||||||
list_to_streams,
|
list_to_streams,
|
||||||
|
user_has_content_access,
|
||||||
)
|
)
|
||||||
from zerver.lib.subscription_info import (
|
from zerver.lib.subscription_info import (
|
||||||
bulk_get_subscriber_user_ids,
|
bulk_get_subscriber_user_ids,
|
||||||
@@ -2125,12 +2126,18 @@ class StreamAdminTest(ZulipTestCase):
|
|||||||
def test_non_admin_cannot_access_unsub_private_stream(self) -> None:
|
def test_non_admin_cannot_access_unsub_private_stream(self) -> None:
|
||||||
iago = self.example_user("iago")
|
iago = self.example_user("iago")
|
||||||
hamlet = self.example_user("hamlet")
|
hamlet = self.example_user("hamlet")
|
||||||
|
nobody_group = NamedUserGroup.objects.get(
|
||||||
|
name="role:nobody", is_system_group=True, realm=hamlet.realm
|
||||||
|
)
|
||||||
|
|
||||||
self.login_user(hamlet)
|
self.login_user(hamlet)
|
||||||
result = self.subscribe_via_post(
|
result = self.subscribe_via_post(
|
||||||
hamlet,
|
hamlet,
|
||||||
["private_stream_1"],
|
["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,
|
invite_only=True,
|
||||||
)
|
)
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
@@ -2669,26 +2676,81 @@ class StreamAdminTest(ZulipTestCase):
|
|||||||
f"'{setting_name}' setting cannot be set to 'role:internet' group.",
|
f"'{setting_name}' setting cannot be set to 'role:internet' group.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# For private streams, even admins must be subscribed to the
|
# For private streams, realm admins need not be subscribed to
|
||||||
# stream to change the setting.
|
# the stream to change the setting as they can administer the
|
||||||
|
# channel by default.
|
||||||
stream = get_stream("stream_name2", realm)
|
stream = get_stream("stream_name2", realm)
|
||||||
params[setting_name] = orjson.dumps({"new": moderators_system_group.id}).decode()
|
params[setting_name] = orjson.dumps({"new": moderators_system_group.id}).decode()
|
||||||
result = self.client_patch(
|
result = self.client_patch(
|
||||||
f"/json/streams/{stream.id}",
|
f"/json/streams/{stream.id}",
|
||||||
params,
|
params,
|
||||||
)
|
)
|
||||||
|
if setting_name in Stream.stream_permission_group_settings_requiring_content_access:
|
||||||
self.assert_json_error(result, "Invalid channel ID")
|
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(
|
result = self.client_patch(
|
||||||
f"/json/streams/{stream.id}",
|
f"/json/streams/{stream.id}",
|
||||||
params,
|
params,
|
||||||
)
|
)
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
stream = get_stream("stream_name2", realm)
|
stream = get_stream("stream_name2", realm)
|
||||||
self.assertEqual(getattr(stream, setting_name).id, moderators_system_group.id)
|
self.assertEqual(getattr(stream, setting_name).id, owners_group.id)
|
||||||
# Unsubscribe user from private stream to test next setting.
|
else:
|
||||||
self.unsubscribe(user_profile, "stream_name2")
|
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:
|
def test_changing_stream_permission_settings(self) -> None:
|
||||||
self.make_stream("stream_name1")
|
self.make_stream("stream_name1")
|
||||||
@@ -7602,6 +7664,72 @@ class AccessStreamTest(ZulipTestCase):
|
|||||||
assert sub_ret is None
|
assert sub_ret is None
|
||||||
self.assertEqual(stream.id, stream_ret.id)
|
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):
|
class StreamTrafficTest(ZulipTestCase):
|
||||||
def test_average_weekly_stream_traffic_calculation(self) -> None:
|
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_default_stream_group_by_id,
|
||||||
access_stream_by_id,
|
access_stream_by_id,
|
||||||
access_stream_by_name,
|
access_stream_by_name,
|
||||||
access_stream_for_delete_or_update,
|
access_stream_for_delete_or_update_requiring_metadata_access,
|
||||||
access_web_public_stream,
|
access_web_public_stream,
|
||||||
check_stream_name_available,
|
check_stream_name_available,
|
||||||
do_get_streams,
|
do_get_streams,
|
||||||
@@ -75,6 +75,7 @@ from zerver.lib.streams import (
|
|||||||
get_stream_permission_policy_name,
|
get_stream_permission_policy_name,
|
||||||
list_to_streams,
|
list_to_streams,
|
||||||
stream_to_dict,
|
stream_to_dict,
|
||||||
|
user_has_content_access,
|
||||||
)
|
)
|
||||||
from zerver.lib.subscription_info import gather_subscriptions
|
from zerver.lib.subscription_info import gather_subscriptions
|
||||||
from zerver.lib.topic import (
|
from zerver.lib.topic import (
|
||||||
@@ -142,7 +143,9 @@ def user_directly_controls_user(user_profile: UserProfile, target: UserProfile)
|
|||||||
def deactivate_stream_backend(
|
def deactivate_stream_backend(
|
||||||
request: HttpRequest, user_profile: UserProfile, stream_id: int
|
request: HttpRequest, user_profile: UserProfile, stream_id: int
|
||||||
) -> HttpResponse:
|
) -> 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)
|
do_deactivate_stream(stream, acting_user=user_profile)
|
||||||
return json_success(request)
|
return json_success(request)
|
||||||
|
|
||||||
@@ -266,9 +269,12 @@ def update_stream_backend(
|
|||||||
can_send_message_group: Json[GroupSettingChangeRequest] | None = None,
|
can_send_message_group: Json[GroupSettingChangeRequest] | None = None,
|
||||||
can_remove_subscribers_group: Json[GroupSettingChangeRequest] | None = None,
|
can_remove_subscribers_group: Json[GroupSettingChangeRequest] | None = None,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
# We allow realm administrators to update the stream name and
|
# Most settings updates only require metadata access, not content
|
||||||
# description even for private streams.
|
# access. We will check for content access further when and where
|
||||||
(stream, sub) = access_stream_for_delete_or_update(user_profile, stream_id)
|
# 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.
|
# Validate that the proposed state for permissions settings is permitted.
|
||||||
if is_private is not None:
|
if is_private is not None:
|
||||||
@@ -325,7 +331,7 @@ def update_stream_backend(
|
|||||||
raise JsonableError(_("Moderation request channel must be private."))
|
raise JsonableError(_("Moderation request channel must be private."))
|
||||||
|
|
||||||
if is_private is not None:
|
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
|
# subscribed to make a private stream public, via this
|
||||||
# stricted access_stream check.
|
# stricted access_stream check.
|
||||||
access_stream_by_id(user_profile, stream_id)
|
access_stream_by_id(user_profile, stream_id)
|
||||||
@@ -405,8 +411,10 @@ def update_stream_backend(
|
|||||||
if validate_group_setting_value_change(
|
if validate_group_setting_value_change(
|
||||||
current_setting_api_value, new_setting_value, expected_current_setting_value
|
current_setting_api_value, new_setting_value, expected_current_setting_value
|
||||||
):
|
):
|
||||||
if sub is None and stream.invite_only:
|
if (
|
||||||
# Admins cannot change this setting for unsubscribed private streams.
|
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"))
|
raise JsonableError(_("Invalid channel ID"))
|
||||||
with transaction.atomic(durable=True):
|
with transaction.atomic(durable=True):
|
||||||
user_group = access_user_group_for_setting(
|
user_group = access_user_group_for_setting(
|
||||||
|
|||||||
Reference in New Issue
Block a user