settings: Create an explicit invite_to_stream_policy setting.

This commit creates a new organization setting that determines whether
a user can invite other users to streams. Previously this was linked
to the waiting period threshold, but this was both not documented and
overly limiting.

With significant tweaks by tabbott to change the database model to not
involve two threshhold fields, edit the tests, etc.

This requires follow-up work to make the create stream policy setting
work how this code implies it should.

Fixes #12042.
This commit is contained in:
David Wood 2019-04-08 18:23:00 +01:00 committed by Tim Abbott
parent 89ada6c770
commit 272ed90685
20 changed files with 268 additions and 28 deletions

View File

@ -123,14 +123,13 @@ casper.then(function () {
});
casper.waitUntilVisible('#id_realm_create_stream_permission', function () {
// Test Setting was saved
// Test setting was saved
casper.test.assertEval(function () {
return $('input[type="text"][id="id_realm_waiting_period_threshold"]').val() === '6';
}, 'Waiting period threshold set to 6 days');
// Deactivate setting
casper.evaluate(function () {
$("#id_realm_create_stream_permission").val("by_admins_only").change();
});

View File

@ -200,11 +200,11 @@ var event_fixtures = {
value: false,
},
realm__update__invite_by_admins_only: {
realm__update__invite_to_stream_policy: {
type: 'realm',
op: 'update',
property: 'invite_by_admins_only',
value: false,
property: 'invite_to_stream_policy',
value: 2,
},
realm__update__invite_required: {
@ -875,8 +875,8 @@ with_overrides(function (override) {
var event = event_fixtures.realm__update__create_stream_by_admins_only;
test_realm_boolean(event, 'realm_create_stream_by_admins_only');
event = event_fixtures.realm__update__invite_by_admins_only;
test_realm_boolean(event, 'realm_invite_by_admins_only');
event = event_fixtures.realm__update__invite_to_stream_policy;
test_realm_boolean(event, 'realm_invite_to_stream_policy');
event = event_fixtures.realm__update__invite_required;
test_realm_boolean(event, 'realm_invite_required');

View File

@ -234,6 +234,12 @@ function test_submit_settings_form(submit_form) {
};
$("#id_realm_create_stream_permission").val("by_anyone");
$("#id_realm_add_emoji_by_admins_only").val("by_anyone");
const invite_to_stream_policy_elem = $("#id_realm_invite_to_stream_policy");
invite_to_stream_policy_elem.val("1");
invite_to_stream_policy_elem.attr('id', 'id_realm_invite_to_stream_policy');
invite_to_stream_policy_elem.data = () => {
return "integer";
};
const bot_creation_policy_elem = $("#id_realm_bot_creation_policy");
bot_creation_policy_elem.val("1");
bot_creation_policy_elem.attr('id', 'id_realm_bot_creation_policy');
@ -250,6 +256,7 @@ function test_submit_settings_form(submit_form) {
let subsection_elem = $(`#org-${subsection}`);
subsection_elem.set_find_results('.setting-widget', [
bot_creation_policy_elem,
invite_to_stream_policy_elem,
email_address_visibility_elem,
]);
@ -259,6 +266,7 @@ function test_submit_settings_form(submit_form) {
let expected_value = {
bot_creation_policy: '1',
invite_to_stream_policy: '1',
email_address_visibility: '1',
add_emoji_by_admins_only: false,
create_stream_by_admins_only: false,
@ -532,6 +540,18 @@ function test_sync_realm_settings() {
assert.equal(waiting_period_input_parent.visible(), false);
}
{
/* Test invite to stream policy settings sync */
const property_elem = $('#id_realm_invite_to_stream_policy');
property_elem.length = 1;
property_elem.attr('id', 'id_realm_invite_to_stream_policy');
page_params.realm_invite_to_stream_policy = 3;
settings_org.sync_realm_settings('invite_to_stream_policy');
assert.equal($("#id_realm_invite_to_stream_policy").val(), "by_members_with_waiting_period");
}
{
/* Test message content edit limit minutes sync */
const property_elem = $('#id_realm_message_content_edit_limit_minutes');
@ -743,8 +763,8 @@ run_test('set_up', () => {
$("#enable_digest_emails_label").set_parent($.create('<stub digest setting checkbox>'));
$("#id_realm_msg_edit_limit_setting").change = noop;
$('#id_realm_msg_delete_limit_setting').change = noop;
const parent_elem = $.create('waiting-period-parent-stub');
$('#id_realm_waiting_period_threshold').set_parent(parent_elem);
const waiting_period_parent_elem = $.create('waiting-period-parent-stub');
$('#id_realm_waiting_period_threshold').set_parent(waiting_period_parent_elem);
$("#allowed_domains_label").set_parent($.create('<stub-allowed-domain-label-parent>'));
const allow_topic_edit_label_parent = $.create('allow-topic-edit-label-parent');

View File

@ -32,6 +32,7 @@ exports.build_page = function () {
server_inline_url_embed_preview: page_params.server_inline_url_embed_preview,
realm_authentication_methods: page_params.realm_authentication_methods,
realm_create_stream_by_admins_only: page_params.realm_create_stream_by_admins_only,
realm_invite_to_stream_by_admins_only: page_params.realm_invite_to_stream_by_admins_only,
realm_name_changes_disabled: page_params.realm_name_changes_disabled,
realm_email_changes_disabled: page_params.realm_email_changes_disabled,
realm_add_emoji_by_admins_only: page_params.realm_add_emoji_by_admins_only,

View File

@ -92,6 +92,7 @@ exports.dispatch_normal_event = function dispatch_normal_event(event) {
allow_community_topic_editing: noop,
bot_creation_policy: settings_bots.update_bot_permissions_ui,
create_stream_by_admins_only: noop,
invite_to_stream_policy: noop,
default_language: noop,
default_twenty_four_hour_time: noop,
description: noop,
@ -131,6 +132,12 @@ exports.dispatch_normal_event = function dispatch_normal_event(event) {
page_params.can_create_streams =
!page_params.realm_create_stream_by_admins_only;
}
} else if (event.property === 'invite_to_stream_policy') {
if (!page_params.is_admin) {
// TODO: Add waiting_period_threshold logic here.
page_params.can_invite_to_stream =
page_params.realm_invite_to_stream_policy === 1;
}
} else if (event.property === 'notifications_stream_id') {
settings_org.render_notifications_stream_ui(
page_params.realm_notifications_stream_id,

View File

@ -83,6 +83,18 @@ function get_property_value(property_name) {
return "by_admin_user_with_custom_time";
}
if (property_name === 'realm_invite_to_stream_policy') {
if (page_params.realm_invite_to_stream_policy === 1) {
return "by_members";
}
if (page_params.realm_invite_to_stream_policy === 2) {
return "by_admins_only";
}
if (page_params.realm_invite_to_stream_policy === 3) {
return "by_members_with_waiting_period";
}
}
if (property_name === 'realm_add_emoji_by_admins_only') {
if (page_params.realm_add_emoji_by_admins_only) {
return "by_admins_only";
@ -153,6 +165,11 @@ function set_create_stream_permission_dropdown() {
}
}
function set_invite_to_stream_policy_dropdown() {
var value = get_property_value("realm_invite_to_stream_policy");
$("#id_realm_invite_to_stream_policy").val(value);
}
function set_add_emoji_permission_dropdown() {
$("#id_realm_add_emoji_by_admins_only").val(get_property_value("realm_add_emoji_by_admins_only"));
}
@ -392,6 +409,8 @@ exports.populate_signup_notifications_stream_dropdown = function (stream_list) {
function update_dependent_subsettings(property_name) {
if (property_name === 'realm_create_stream_permission' || property_name === 'realm_waiting_period_threshold') {
set_create_stream_permission_dropdown();
} else if (property_name === 'realm_invite_to_stream_policy') {
set_invite_to_stream_policy_dropdown();
} else if (property_name === 'realm_video_chat_provider' ||
property_name === 'realm_google_hangouts_domain' ||
property_name.startsWith('realm_zoom')) {
@ -438,6 +457,8 @@ exports.sync_realm_settings = function (property) {
property = 'message_content_edit_limit_minutes';
} else if (property === 'create_stream_by_admins_only') {
property = 'create_stream_permission';
} else if (property === 'invite_to_stream_policy') {
property = 'invite_to_stream_policy';
} else if (property === 'allow_message_editing') {
property = 'msg_edit_limit_setting';
} else if (property === 'emails_restricted_to_domains' || property === 'disallow_disposable_email_addresses') {
@ -566,6 +587,7 @@ exports.build_page = function () {
}
set_create_stream_permission_dropdown();
set_invite_to_stream_policy_dropdown();
set_add_emoji_permission_dropdown();
set_video_chat_provider_dropdown();
set_msg_edit_limit_dropdown();
@ -692,6 +714,7 @@ exports.build_page = function () {
JSON.stringify(parseInt(new_message_retention_days, 10)) : null;
} else if (subsection === 'other_permissions') {
var create_stream_permission = $("#id_realm_create_stream_permission").val();
var invite_to_stream_policy = $("#id_realm_invite_to_stream_policy").val();
var add_emoji_permission = $("#id_realm_add_emoji_by_admins_only").val();
if (add_emoji_permission === "by_admins_only") {
@ -712,6 +735,14 @@ exports.build_page = function () {
data.create_stream_by_admins_only = false;
data.waiting_period_threshold = 0;
}
if (invite_to_stream_policy === "by_admins_only") {
data.invite_to_stream_policy = 2;
} else if (invite_to_stream_policy === "by_members") {
data.invite_to_stream_policy = 1;
} else if (invite_to_stream_policy === "by_members_with_waiting_period") {
data.invite_to_stream_policy = 3;
}
} else if (subsection === 'org_join') {
var org_join_restrictions = $('#id_realm_org_join_restrictions').val();
if (org_join_restrictions === "only_selected_domain") {

View File

@ -1546,6 +1546,7 @@ body:not(.night-mode) #account-settings .custom_user_field .datepicker {
}
#id_realm_create_stream_permission,
#id_realm_invite_to_stream_policy,
#id_realm_org_join_restrictions,
#id_realm_bot_creation_policy,
#id_realm_user_invite_restriction {

View File

@ -64,8 +64,8 @@
<select name="realm_create_stream_permission" id="id_realm_create_stream_permission" class="prop-element">
<option value="by_anyone">{{t "Members and admins" }}</option>
<option value="by_admins_only">{{t "Admins only" }}</option>
<option value="by_admin_user_with_three_days_old">{{t "All admins, and members with accounts at least 3 days old." }}</option>
<option value="by_admin_user_with_custom_time">{{t "All admins, and members with accounts at least N days old." }}</option>
<option value="by_admin_user_with_three_days_old">{{t "All admins, and members with accounts at least 3 days old" }}</option>
<option value="by_admin_user_with_custom_time">{{t "All admins, and members with accounts at least N days old" }}</option>
</select>
</div>
<div class="dependent-block">
@ -76,6 +76,15 @@
value="{{ realm_waiting_period_threshold }}"/>
</div>
<div class="input-group">
<label for="realm_invite_to_stream_policy" class="dropdown-title">{{t "Who can add users to streams" }}</label>
<select name="realm_invite_to_stream_policy" id="id_realm_invite_to_stream_policy" class="prop-element">
<option value="by_members">{{t "Members and admins" }}</option>
<option value="by_admins_only">{{t "Admins only" }}</option>
<option value="by_members">{{t "All admins and members who can create streams" }}</option>
</select>
</div>
<div class="input-group">
<label for="realm_bot_creation_policy">{{t "Who can add bots" }}</label>
<select name="realm_bot_creation_policy" class="setting-widget prop-element" id="id_realm_bot_creation_policy" data-setting-widget-type="integer">

View File

@ -0,0 +1,28 @@
# Restrict stream invitation
{!admin-only.md!}
By default, anyone other than guests can invite others to streams. However, you can restrict stream
invitation to
* **Members and organization administrators**, or
* **Organization administrators**, or
* **Organization administrators, and members with accounts older than the new user waiting period**
For corporations and other entities with controlled access, we highly
recommend keeping stream invitation open.
For entities with lots of streams with confidential contents, it may be desirable
to limit invitations.
### Manage who can create streams
{start_tabs}
{settings_tab|organization-permissions}
2. Under **Other permissions**, configure **Who can invite other users to streams**.
{!save-changes.md!}
{end_tabs}

View File

@ -116,6 +116,7 @@
## Organization settings
* [Restrict stream creation](/help/configure-who-can-create-streams)
* [Restrict stream invitation](/help/configure-who-can-invite-to-streams)
* [Change who can add custom emoji](/help/only-allow-admins-to-add-emoji)
* [Block image and link previews](/help/allow-image-link-previews)
* [Restrict name and email changes](/help/restrict-name-and-email-changes)

View File

@ -571,6 +571,9 @@ def apply_event(state: Dict[str, Any],
if (field in ['realm_create_stream_by_admins_only',
'realm_waiting_period_threshold']) and 'can_create_streams' in state:
state['can_create_streams'] = user_profile.can_create_streams()
if (field in ['realm_invite_to_stream_policy',
'realm_waiting_period_threshold']) and 'can_subscribe_other_users' in state:
state['can_subscribe_other_users'] = user_profile.can_subscribe_other_users()
elif event['op'] == "update_dict":
for key, value in event['data'].items():

View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-04-29 05:29
from __future__ import unicode_literals
from django.db import migrations, models
from django.db.backends.postgresql_psycopg2.schema import DatabaseSchemaEditor
from django.db.migrations.state import StateApps
def handle_waiting_period(apps: StateApps, schema_editor: DatabaseSchemaEditor) -> None:
Realm = apps.get_model('zerver', 'Realm')
Realm.objects.filter(waiting_period_threshold__gt=0).update(
invite_to_stream_policy=3) # INVITE_TO_STREAM_POLICY_WAITING_PERIOD
class Migration(migrations.Migration):
dependencies = [
('zerver', '0213_realm_digest_weekday'),
]
operations = [
migrations.AddField(
model_name='realm',
name='invite_to_stream_policy',
field=models.PositiveSmallIntegerField(default=1),
),
migrations.RunPython(handle_waiting_period,
reverse_code=migrations.RunPython.noop),
]

View File

@ -201,6 +201,13 @@ class Realm(models.Model):
name_changes_disabled = models.BooleanField(default=False) # type: bool
email_changes_disabled = models.BooleanField(default=False) # type: bool
# Who in the organization is allowed to invite other users to streams.
INVITE_TO_STREAM_POLICY_MEMBERS = 1
INVITE_TO_STREAM_POLICY_ADMINS = 2
INVITE_TO_STREAM_POLICY_WAITING_PERIOD = 3
invite_to_stream_policy = models.PositiveSmallIntegerField(
default=INVITE_TO_STREAM_POLICY_MEMBERS) # type: bool
# Who in the organization has access to users' actual email
# addresses. Controls whether the UserProfile.email field is the
# same as UserProfile.delivery_email, or is instead garbage.
@ -291,6 +298,7 @@ class Realm(models.Model):
allow_message_deleting=bool,
bot_creation_policy=int,
create_stream_by_admins_only=bool,
invite_to_stream_policy=int,
default_language=str,
default_twenty_four_hour_time = bool,
description=str,
@ -1004,9 +1012,15 @@ class UserProfile(AbstractBaseUser, PermissionsMixin):
def can_subscribe_other_users(self) -> bool:
if self.is_realm_admin:
return True
if self.realm.invite_to_stream_policy == Realm.INVITE_TO_STREAM_POLICY_ADMINS:
return False
if self.is_guest:
return False
if self.realm.invite_to_stream_policy == Realm.INVITE_TO_STREAM_POLICY_MEMBERS:
return True
assert self.realm.invite_to_stream_policy == Realm.INVITE_TO_STREAM_POLICY_WAITING_PERIOD
diff = (timezone_now() - self.date_joined).days
if diff >= self.realm.waiting_period_threshold:
return True

View File

@ -359,6 +359,8 @@ paths:
* `realm_create_stream_by_admins_only`:
* `realm_invite_to_stream_policy`:
* `realm_default_language`:
* `realm_default_streams`:
@ -892,6 +894,8 @@ definitions:
type: string
realm_create_stream_by_admins_only:
type: boolean
realm_invite_to_stream_policy:
type: int64
realm_default_language:
type: string
realm_default_streams:

View File

@ -1574,6 +1574,7 @@ class EventsRegisterTest(ZulipTestCase):
message_retention_days=[10, 20],
name=[u'Zulip', u'New Name'],
waiting_period_threshold=[10, 20],
invite_to_stream_policy=[3, 2, 1],
email_address_visibility=[Realm.EMAIL_ADDRESS_VISIBILITY_ADMINS],
bot_creation_policy=[Realm.BOT_CREATION_EVERYONE],
video_chat_provider=[u'Google Hangouts', u'Jitsi'],

View File

@ -148,6 +148,7 @@ class HomeTest(ZulipTestCase):
"realm_inline_url_embed_preview",
"realm_invite_by_admins_only",
"realm_invite_required",
"realm_invite_to_stream_policy",
"realm_is_zephyr_mirror_realm",
"realm_logo_source",
"realm_logo_url",

View File

@ -548,6 +548,9 @@ class RealmAPITest(ZulipTestCase):
message_retention_days=[10, 20],
name=[u'Zulip', u'New Name'],
waiting_period_threshold=[10, 20],
invite_to_stream_policy=[Realm.INVITE_TO_STREAM_POLICY_ADMINS,
Realm.INVITE_TO_STREAM_POLICY_MEMBERS,
Realm.INVITE_TO_STREAM_POLICY_WAITING_PERIOD],
bot_creation_policy=[1, 2],
email_address_visibility=[Realm.EMAIL_ADDRESS_VISIBILITY_EVERYONE,
Realm.EMAIL_ADDRESS_VISIBILITY_ADMINS],

View File

@ -52,7 +52,7 @@ from zerver.lib.actions import (
do_create_realm, do_remove_default_stream, bulk_get_subscriber_user_ids,
gather_subscriptions_helper, bulk_add_subscriptions, bulk_remove_subscriptions,
gather_subscriptions, get_default_streams_for_realm, get_realm, get_stream,
do_get_streams,
do_get_streams, do_change_is_guest,
create_stream_if_needed, create_streams_if_needed,
ensure_stream,
do_deactivate_stream,
@ -1014,6 +1014,60 @@ class StreamAdminTest(ZulipTestCase):
)
self.assert_json_success(result)
def test_invite_to_stream_by_invite_period_threshold(self) -> None:
"""
Non admin users with account age greater or equal to the invite
to stream threshold should be able to invite others to a stream.
"""
hamlet_user = self.example_user('hamlet')
hamlet_user.date_joined = timezone_now()
hamlet_user.save()
cordelia_user = self.example_user('cordelia')
cordelia_user.date_joined = timezone_now()
cordelia_user.save()
do_set_realm_property(hamlet_user.realm, 'invite_to_stream_policy',
Realm.INVITE_TO_STREAM_POLICY_WAITING_PERIOD)
hamlet_email = hamlet_user.email
cordelia_email = cordelia_user.email
self.login(hamlet_email)
do_change_is_admin(hamlet_user, True)
# Hamlet creates a stream as an admin..
stream_name = ['waitingperiodtest']
result = self.common_subscribe_to_streams(hamlet_email, stream_name)
self.assert_json_success(result)
# Can only invite users to stream if their account is ten days old..
do_change_is_admin(hamlet_user, False)
do_set_realm_property(hamlet_user.realm, 'waiting_period_threshold', 10)
# Attempt and fail to invite Cordelia to the stream..
result = self.common_subscribe_to_streams(hamlet_email, stream_name, {"principals": ujson.dumps([cordelia_email])})
self.assert_json_error(result,
"Your account is too new to modify other users' subscriptions.")
# Anyone can invite users..
do_set_realm_property(hamlet_user.realm, 'waiting_period_threshold', 0)
# Attempt and succeed to invite Cordelia to the stream..
result = self.common_subscribe_to_streams(hamlet_email, stream_name, {"principals": ujson.dumps([cordelia_email])})
self.assert_json_success(result)
# Set threshold to 20 days..
do_set_realm_property(hamlet_user.realm, 'waiting_period_threshold', 20)
# Make Hamlet's account 21 days old..
hamlet_user.date_joined = timezone_now() - timedelta(days=21)
hamlet_user.save()
# Unsubscribe Cordelia..
self.unsubscribe(cordelia_user, stream_name[0])
# Attempt and succeed to invite Aaron to the stream..
result = self.common_subscribe_to_streams(hamlet_email, stream_name, {"principals": ujson.dumps([cordelia_email])})
self.assert_json_success(result)
def test_remove_already_not_subbed(self) -> None:
"""
Trying to unsubscribe someone who already isn't subscribed to a stream
@ -2090,31 +2144,59 @@ class SubscriptionAPITest(ZulipTestCase):
def test_user_settings_for_subscribing_other_users(self) -> None:
"""
You can't subscribe other people to streams if you are a guest or your waiting period is not over.
You can't subscribe other people to streams if you are a guest or your account is not old
enough.
"""
invitee_email = self.example_email("cordelia")
with mock.patch('zerver.models.UserProfile.can_subscribe_other_users', return_value=False):
result = self.common_subscribe_to_streams(self.test_email, ['stream1'], {"principals": ujson.dumps([invitee_email])})
self.assert_json_error(result, "Your account is too new to modify other users' subscriptions.")
user_profile = self.example_user("cordelia")
invitee_email = user_profile.email
realm = user_profile.realm
with mock.patch('zerver.models.UserProfile.can_subscribe_other_users', return_value=True):
result = self.common_subscribe_to_streams(self.test_email, ['stream2'], {"principals": ujson.dumps([invitee_email])})
self.assert_json_success(result)
do_set_realm_property(realm, "create_stream_by_admins_only", False)
do_set_realm_property(realm, "invite_to_stream_policy",
Realm.INVITE_TO_STREAM_POLICY_ADMINS)
result = self.common_subscribe_to_streams(
self.test_email, ['stream1'], {"principals": ujson.dumps([invitee_email])})
self.assert_json_error(
result, "Only administrators can modify other users' subscriptions.")
do_set_realm_property(realm, "invite_to_stream_policy",
Realm.INVITE_TO_STREAM_POLICY_MEMBERS)
result = self.common_subscribe_to_streams(
self.test_email, ['stream2'], {"principals": ujson.dumps([
self.test_email, invitee_email])})
self.assert_json_success(result)
self.unsubscribe(user_profile, "stream2")
do_set_realm_property(realm, "invite_to_stream_policy",
Realm.INVITE_TO_STREAM_POLICY_WAITING_PERIOD)
do_set_realm_property(realm, "waiting_period_threshold", 100000)
result = self.common_subscribe_to_streams(
self.test_email, ['stream2'], {"principals": ujson.dumps([invitee_email])})
self.assert_json_error(
result, "Your account is too new to modify other users' subscriptions.")
do_set_realm_property(realm, "waiting_period_threshold", 0)
result = self.common_subscribe_to_streams(
self.test_email, ['stream2'], {"principals": ujson.dumps([invitee_email])})
self.assert_json_success(result)
def test_can_subscribe_other_users(self) -> None:
"""
You can't subscribe other people to streams if you are a guest or your waiting period is not over.
You can't subscribe other people to streams if you are a guest or your account is not old
enough.
"""
othello = self.example_user('othello')
othello.is_realm_admin = True
do_change_is_admin(othello, True)
self.assertTrue(othello.can_subscribe_other_users())
othello.is_realm_admin = False
othello.is_guest = True
do_change_is_admin(othello, False)
do_change_is_guest(othello, True)
self.assertFalse(othello.can_subscribe_other_users())
othello.is_guest = False
othello.realm.waiting_period_threshold = 1000
do_change_is_guest(othello, False)
do_set_realm_property(othello.realm, "waiting_period_threshold", 1000)
do_set_realm_property(othello.realm, "invite_to_stream_policy",
Realm.INVITE_TO_STREAM_POLICY_WAITING_PERIOD)
othello.date_joined = timezone_now() - timedelta(days=(othello.realm.waiting_period_threshold - 1))
self.assertFalse(othello.can_subscribe_other_users())

View File

@ -61,6 +61,7 @@ def update_realm(
message_content_allowed_in_email_notifications:
Optional[bool]=REQ(validator=check_bool, default=None),
bot_creation_policy: Optional[int]=REQ(converter=to_not_negative_int_or_none, default=None),
invite_to_stream_policy: Optional[int]=REQ(validator=check_int, default=None),
email_address_visibility: Optional[int]=REQ(converter=to_not_negative_int_or_none, default=None),
default_twenty_four_hour_time: Optional[bool]=REQ(validator=check_bool, default=None),
video_chat_provider: Optional[str]=REQ(validator=check_string, default=None),

View File

@ -30,8 +30,7 @@ from zerver.lib.streams import access_stream_by_id, access_stream_by_name, \
from zerver.lib.topic import get_topic_history_for_stream, messages_for_topic
from zerver.lib.validator import check_string, check_int, check_list, check_dict, \
check_bool, check_variable_type, check_capped_string, check_color, check_dict_only
from zerver.models import UserProfile, Stream, \
UserMessage, \
from zerver.models import UserProfile, Stream, Realm, UserMessage, \
get_system_bot, get_active_user
from collections import defaultdict
@ -332,6 +331,12 @@ def add_subscriptions_backend(
if user_profile.realm.is_zephyr_mirror_realm and not all(stream.invite_only for stream in streams):
return json_error(_("You can only invite other Zephyr mirroring users to private streams."))
if not user_profile.can_subscribe_other_users():
if user_profile.realm.invite_to_stream_policy == Realm.INVITE_TO_STREAM_POLICY_ADMINS:
return json_error(_("Only administrators can modify other users' subscriptions."))
# Realm.INVITE_TO_STREAM_POLICY_MEMBERS only fails if the
# user is a guest, which happens in the decorator above.
assert user_profile.realm.invite_to_stream_policy == \
Realm.INVITE_TO_STREAM_POLICY_WAITING_PERIOD
return json_error(_("Your account is too new to modify other users' subscriptions."))
subscribers = set(principal_to_user_profile(user_profile, principal) for principal in principals)
else: