org settings: Add setting to prevent users from adding bots.

Fixes: #7908.
This commit is contained in:
Shubham Dhama 2018-01-29 20:40:54 +05:30 committed by Tim Abbott
parent 7c830c5767
commit 777b6de689
20 changed files with 252 additions and 49 deletions

View File

@ -54,6 +54,10 @@ set_global('message_edit', {
update_message_topic_editing_pencil: noop, update_message_topic_editing_pencil: noop,
}); });
set_global('settings_bots', {
update_bot_permissions_ui: noop,
});
// page_params is highly coupled to dispatching now // page_params is highly coupled to dispatching now
set_global('page_params', {test_suite: false}); set_global('page_params', {test_suite: false});
var page_params = global.page_params; var page_params = global.page_params;
@ -195,11 +199,11 @@ var event_fixtures = {
value: false, value: false,
}, },
realm__update__create_generic_bot_by_admins_only: { realm__update__bot_creation_policy: {
type: 'realm', type: 'realm',
op: 'update', op: 'update',
property: 'create_generic_bot_by_admins_only', property: 'bot_creation_policy',
value: false, value: 1,
}, },
realm__update_dict__default: { realm__update_dict__default: {

View File

@ -41,8 +41,6 @@ function _setup_page() {
realm_email_changes_disabled: page_params.realm_email_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, realm_add_emoji_by_admins_only: page_params.realm_add_emoji_by_admins_only,
can_admin_emojis: page_params.is_admin || !page_params.realm_add_emoji_by_admins_only, can_admin_emojis: page_params.is_admin || !page_params.realm_add_emoji_by_admins_only,
realm_create_generic_bot_by_admins_only:
page_params.realm_create_generic_bot_by_admins_only,
realm_allow_message_deleting: page_params.realm_allow_message_deleting, realm_allow_message_deleting: page_params.realm_allow_message_deleting,
realm_allow_message_editing: page_params.realm_allow_message_editing, realm_allow_message_editing: page_params.realm_allow_message_editing,
realm_message_content_edit_limit_minutes: realm_message_content_edit_limit_minutes:
@ -62,10 +60,14 @@ function _setup_page() {
realm_send_welcome_emails: page_params.realm_send_welcome_emails, realm_send_welcome_emails: page_params.realm_send_welcome_emails,
}; };
options.bot_creation_policy_values = settings_bots.bot_creation_policy_values;
var admin_tab = templates.render('admin_tab', options); var admin_tab = templates.render('admin_tab', options);
$("#settings_content .organization-box").html(admin_tab); $("#settings_content .organization-box").html(admin_tab);
$("#settings_content .alert").removeClass("show"); $("#settings_content .alert").removeClass("show");
settings_bots.update_bot_settings_tip();
$("#id_realm_bot_creation_policy").val(page_params.realm_bot_creation_policy);
// Since we just swapped in a whole new page, we need to // Since we just swapped in a whole new page, we need to
// tell admin_sections nothing is loaded. // tell admin_sections nothing is loaded.
admin_sections.reset_sections(); admin_sections.reset_sections();

View File

@ -56,7 +56,7 @@ exports.dispatch_normal_event = function dispatch_normal_event(event) {
allow_edit_history: noop, allow_edit_history: noop,
allow_message_deleting: noop, allow_message_deleting: noop,
allow_message_editing: noop, allow_message_editing: noop,
create_generic_bot_by_admins_only: noop, bot_creation_policy: settings_bots.update_bot_permissions_ui,
create_stream_by_admins_only: noop, create_stream_by_admins_only: noop,
default_language: settings_org.reset_realm_default_language, default_language: settings_org.reset_realm_default_language,
description: settings_org.update_realm_description, description: settings_org.update_realm_description,

View File

@ -99,12 +99,17 @@ function _setup_page() {
return tab; return tab;
}()); }());
settings_bots.setup_bot_creation_policy_values();
var settings_tab = templates.render('settings_tab', { var settings_tab = templates.render('settings_tab', {
full_name: people.my_full_name(), full_name: people.my_full_name(),
page_params: page_params, page_params: page_params,
zuliprc: 'zuliprc', zuliprc: 'zuliprc',
flaskbotrc: 'flaskbotrc', flaskbotrc: 'flaskbotrc',
timezones: moment.tz.names(), timezones: moment.tz.names(),
admin_only_bot_creation: page_params.is_admin ||
page_params.realm_bot_creation_policy !==
settings_bots.bot_creation_policy_values.admins_only.code,
}); });
$(".settings-box").html(settings_tab); $(".settings-box").html(settings_tab);

View File

@ -83,6 +83,57 @@ exports.generate_flaskbotrc_content = function (email, api_key) {
"\n"; "\n";
}; };
exports.bot_creation_policy_values = {};
exports.setup_bot_creation_policy_values = function () {
exports.bot_creation_policy_values = {
everyone: {
code: 1,
description: i18n.t("Everyone"),
},
admins_only: {
code: 3,
description: i18n.t("Admins only"),
},
restricted: {
code: 2,
description: i18n.t("Everyone, but only admins can add generic bots"),
},
};
};
exports.update_bot_settings_tip = function () {
var permission_type = exports.bot_creation_policy_values;
var current_permission = page_params.realm_bot_creation_policy;
var tip_text;
if (current_permission === permission_type.admins_only.code) {
tip_text = i18n.t("Only organization administrators can add bots to this organization");
} else if (current_permission === permission_type.restricted.code) {
tip_text = i18n.t("Only orgainzation administrators can add generic bots");
} else {
tip_text = i18n.t("Anyone in this organization can add bots");
}
$(".bot-settings-tip").text(tip_text);
};
exports.update_bot_permissions_ui = function () {
exports.update_bot_settings_tip();
$('#bot_table_error').hide();
$("#id_realm_bot_creation_policy").val(page_params.realm_bot_creation_policy);
if (page_params.realm_bot_creation_policy ===
exports.bot_creation_policy_values.admins_only.code &&
!page_params.is_admin) {
$('#create_bot_form').hide();
$('.add-a-new-bot-tab').hide();
$('.account-api-key-section').hide();
$("#bots_lists_navbar .active-bots-tab").click();
} else {
$('#create_bot_form').show();
$('.add-a-new-bot-tab').show();
$('.account-api-key-section').show();
}
};
exports.set_up = function () { exports.set_up = function () {
$('#payload_url_inputbox').hide(); $('#payload_url_inputbox').hide();
$('#create_payload_url').val(''); $('#create_payload_url').val('');
@ -393,6 +444,7 @@ exports.set_up = function () {
$("#add-a-new-bot-form").hide(); $("#add-a-new-bot-form").hide();
$("#active_bots_list").show(); $("#active_bots_list").show();
$("#inactive_bots_list").hide(); $("#inactive_bots_list").hide();
$('#bot_table_error').hide();
}); });
$("#bots_lists_navbar .inactive-bots-tab").click(function (e) { $("#bots_lists_navbar .inactive-bots-tab").click(function (e) {

View File

@ -309,10 +309,9 @@ function _set_up() {
checked_msg: i18n.t("Only administrators may now add new emoji!"), checked_msg: i18n.t("Only administrators may now add new emoji!"),
unchecked_msg: i18n.t("Any user may now add new emoji!"), unchecked_msg: i18n.t("Any user may now add new emoji!"),
}, },
create_generic_bot_by_admins_only: { bot_creation_policy: {
type: 'bool', type: 'integer',
checked_msg: i18n.t("Only administrators may now create new generic bots!"), msg: i18n.t("Permissions changed"),
unchecked_msg: i18n.t("Any user may now create new generic bots!"),
}, },
}, },
}; };
@ -361,7 +360,7 @@ function _set_up() {
return; return;
} }
if (setting_type === 'text') { if (setting_type === 'text' || setting_type === 'integer') {
ui_report.success(field_info.msg, ui_report.success(field_info.msg,
property_type_status_element(key)); property_type_status_element(key));
return; return;

View File

@ -353,7 +353,7 @@ input[type=checkbox] + .inline-block {
margin-top: 10px; margin-top: 10px;
} }
.organization-submission { .org-settings-form .organization-submission {
margin-top: 0px; margin-top: 0px;
} }
@ -1192,7 +1192,8 @@ input[type=checkbox].inline-block {
transform: translateY(-50%); transform: translateY(-50%);
} }
#id_realm_create_stream_permission { #id_realm_create_stream_permission,
#id_realm_bot_creation_policy {
width: 100%; width: 100%;
} }

View File

@ -1,4 +1,5 @@
<div id="admin-bot-list" class="settings-section" data-name="bot-list-admin"> <div id="admin-bot-list" class="settings-section" data-name="bot-list-admin">
<div class="tip bot-settings-tip"></div>
<input type="text" class="search" placeholder="{{t 'Filter bots' }}" aria-label="{{t 'Filter bots' }}"/> <input type="text" class="search" placeholder="{{t 'Filter bots' }}" aria-label="{{t 'Filter bots' }}"/>
<div class="clear-float"></div> <div class="clear-float"></div>
<table class="table table-condensed table-striped wrapped-table"> <table class="table table-condensed table-striped wrapped-table">

View File

@ -3,6 +3,7 @@
<div class="tip"> <div class="tip">
{{#tr this}}Looking for our <a href="/integrations" target="_blank">Integrations</a> or <a href="/api" target="_blank">API</a> documentation?{{/tr}} {{#tr this}}Looking for our <a href="/integrations" target="_blank">Integrations</a> or <a href="/api" target="_blank">API</a> documentation?{{/tr}}
</div> </div>
<div class="tip bot-settings-tip"></div>
<div> <div>
<span>{{t 'Download config of all active outgoing webhook bots in Zulip Botserver format.' }}</span> <span>{{t 'Download config of all active outgoing webhook bots in Zulip Botserver format.' }}</span>
@ -14,7 +15,7 @@
<ul class="nav nav-tabs nav-justified" id="bots_lists_navbar"> <ul class="nav nav-tabs nav-justified" id="bots_lists_navbar">
<li class="active active-bots-tab"><a>{{t "Active bots" }}</a></li> <li class="active active-bots-tab"><a>{{t "Active bots" }}</a></li>
<li class="inactive-bots-tab"><a>{{t "Inactive bots" }}</a></li> <li class="inactive-bots-tab"><a>{{t "Inactive bots" }}</a></li>
<li class="add-a-new-bot-tab"><a>{{t "Add a new bot" }}</a></li> <li class="add-a-new-bot-tab {{#unless admin_only_bot_creation}}hide{{/unless}}"><a>{{t "Add a new bot" }}</a></li>
</ul> </ul>
<ol class="bots_list required-text" id="active_bots_list" data-empty="{{t 'You have no active bots.' }}"> <ol class="bots_list required-text" id="active_bots_list" data-empty="{{t 'You have no active bots.' }}">
@ -26,7 +27,8 @@
<div id="bot_table_error" class="alert alert-error hide"></div> <div id="bot_table_error" class="alert alert-error hide"></div>
<div id="add-a-new-bot-form"> <div id="add-a-new-bot-form">
<form id="create_bot_form" class="form-horizontal no-padding"> <form id="create_bot_form"
class="form-horizontal no-padding {{#unless admin_only_bot_creation}}hide{{/unless}}">
<div class="new-bot-form"> <div class="new-bot-form">
<div class="input-group"> <div class="input-group">
<label for="bot_type"> <label for="bot_type">

View File

@ -9,7 +9,7 @@
<div class="alert" id="admin-realm-name-changes-disabled-status"></div> <div class="alert" id="admin-realm-name-changes-disabled-status"></div>
<div class="alert" id="admin-realm-add-emoji-by-admins-only-status"></div> <div class="alert" id="admin-realm-add-emoji-by-admins-only-status"></div>
<div class="alert" id="admin-realm-create-stream-by-admins-only-status"></div> <div class="alert" id="admin-realm-create-stream-by-admins-only-status"></div>
<div class="alert" id="admin-realm-create-generic-bot-by-admins-only-status"></div> <div class="alert" id="admin-realm-bot-creation-policy-status"></div>
<div class="inline-block organization-permissions-parent"> <div class="inline-block organization-permissions-parent">
<div class="input-group admin-restricted-to-domain"> <div class="input-group admin-restricted-to-domain">
@ -74,8 +74,8 @@
</div> </div>
<h3>{{t "Other permissions" }}</h3> <h3>{{t "Other permissions" }}</h3>
<div class="organization-permissions-parent"> <div class="m-10 inline-block organization-permissions-parent">
<div class="inline-block create-stream-dropdown"> <div class="input-group create-stream-dropdown">
<label for="realm_create_stream_permission" class="dropdown-title">{{t "Who can create streams" }}</label> <label for="realm_create_stream_permission" class="dropdown-title">{{t "Who can create streams" }}</label>
<select name="realm_create_stream_permission" id="id_realm_create_stream_permission"> <select name="realm_create_stream_permission" id="id_realm_create_stream_permission">
<option value="by_anyone">{{t "Everyone" }}</option> <option value="by_anyone">{{t "Everyone" }}</option>
@ -91,8 +91,16 @@
class="admin-realm-message-content-edit-limit-minutes" class="admin-realm-message-content-edit-limit-minutes"
value="{{ realm_waiting_period_threshold }}"/> value="{{ realm_waiting_period_threshold }}"/>
</div> </div>
</div>
<div class="m-t-10 inline-block organization-permissions-parent"> <div class="input-group">
<label for="realm_bot_creation_policy">{{t "Who can add bots" }}</label>
<select name="realm_bot_creation_policy" id="id_realm_bot_creation_policy">
{{#each bot_creation_policy_values}}
<option value='{{this.code}}'>{{this.description}}</option>
{{/each}}
</select>
</div>
<div class="inline-block"> <div class="inline-block">
<label for="realm_add_emoji_by_admins_only" class="dropdown-title">{{t "Who can add emoji" }}</label> <label for="realm_add_emoji_by_admins_only" class="dropdown-title">{{t "Who can add emoji" }}</label>
<select name="realm_add_emoji_by_admins_only" id="id_realm_add_emoji_by_admins_only"> <select name="realm_add_emoji_by_admins_only" id="id_realm_add_emoji_by_admins_only">
@ -102,21 +110,6 @@
</div> </div>
</div> </div>
<h3>{{t "Interactive bots" }}</h3>
<div class="inline-block organization-permissions-parent">
<div class="input-group">
<label class="checkbox">
<input type="checkbox" id="id_realm_create_generic_bot_by_admins_only" name="realm_create_generic_bot_by_admins_only"
{{#if realm_create_generic_bot_by_admins_only}}checked="checked"{{/if}} />
<span></span>
</label>
<label for="id_realm_create_generic_bot_by_admins_only" id="id_realm_create_generic_bot_by_admins_only_label" class="inline-block"
title="{{t 'If checked, only administrators may create new generic bots.' }}">
{{t "Prevent users from creating generic bots" }}
</label>
</div>
</div>
{{#if is_admin }} {{#if is_admin }}
<div class="input-group organization-submission"> <div class="input-group organization-submission">
<button type="submit" class="button rounded sea-green"> <button type="submit" class="button rounded sea-green">

View File

@ -40,6 +40,19 @@ def check_valid_bot_config(service_name: str, config_data: Dict[str, str]) -> No
# error message. # error message.
raise JsonableError(_("Invalid configuration data!")) raise JsonableError(_("Invalid configuration data!"))
def check_bot_creation_policy(user_profile: UserProfile, bot_type: int) -> None:
# Realm administrators can always add bot
if user_profile.is_realm_admin:
return
if user_profile.realm.bot_creation_policy == Realm.BOT_CREATION_EVERYONE:
return
if user_profile.realm.bot_creation_policy == Realm.BOT_CREATION_ADMINS_ONLY:
raise JsonableError(_("Must be an organization administrator"))
if user_profile.realm.bot_creation_policy == Realm.BOT_CREATION_LIMIT_GENERIC_BOTS and \
bot_type == UserProfile.DEFAULT_BOT:
raise JsonableError(_("Must be an organization administrator"))
def check_valid_bot_type(user_profile: UserProfile, bot_type: int) -> None: def check_valid_bot_type(user_profile: UserProfile, bot_type: int) -> None:
if bot_type not in user_profile.allowed_bot_types: if bot_type not in user_profile.allowed_bot_types:
raise JsonableError(_('Invalid bot type')) raise JsonableError(_('Invalid bot type'))

View File

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2018-03-09 18:00
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 set_initial_value_for_bot_creation_policy(apps: StateApps, schema_editor: DatabaseSchemaEditor) -> None:
Realm = apps.get_model("zerver", "Realm")
for realm in Realm.objects.all():
if realm.create_generic_bot_by_admins_only:
realm.bot_creation_policy = 2 # BOT_CREATION_LIMIT_GENERIC_BOTS
else:
realm.bot_creation_policy = 1 # BOT_CREATION_EVERYONE
realm.save(update_fields=["bot_creation_policy"])
def reverse_code(apps: StateApps, schema_editor: DatabaseSchemaEditor) -> None:
Realm = apps.get_model("zerver", "Realm")
for realm in Realm.objects.all():
if realm.bot_creation_policy == 1: # BOT_CREATION_EVERYONE
realm.create_generic_bot_by_admins_only = False
else:
realm.create_generic_bot_by_admins_only = True
realm.save(update_fields=["create_generic_bot_by_admins_only"])
class Migration(migrations.Migration):
dependencies = [
('zerver', '0142_userprofile_translate_emoticons'),
]
operations = [
migrations.AddField(
model_name='realm',
name='bot_creation_policy',
field=models.PositiveSmallIntegerField(default=1),
),
migrations.RunPython(set_initial_value_for_bot_creation_policy,
reverse_code=reverse_code),
]

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2018-03-09 21:21
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('zerver', '0143_realm_bot_creation_policy'),
]
operations = [
migrations.RemoveField(
model_name='realm',
name='create_generic_bot_by_admins_only',
),
]

View File

@ -144,7 +144,6 @@ class Realm(models.Model):
inline_url_embed_preview = models.BooleanField(default=True) # type: bool inline_url_embed_preview = models.BooleanField(default=True) # type: bool
create_stream_by_admins_only = models.BooleanField(default=False) # type: bool create_stream_by_admins_only = models.BooleanField(default=False) # type: bool
add_emoji_by_admins_only = models.BooleanField(default=False) # type: bool add_emoji_by_admins_only = models.BooleanField(default=False) # type: bool
create_generic_bot_by_admins_only = models.BooleanField(default=False) # type: bool
mandatory_topics = models.BooleanField(default=False) # type: bool mandatory_topics = models.BooleanField(default=False) # type: bool
show_digest_email = models.BooleanField(default=True) # type: bool show_digest_email = models.BooleanField(default=True) # type: bool
name_changes_disabled = models.BooleanField(default=False) # type: bool name_changes_disabled = models.BooleanField(default=False) # type: bool
@ -164,6 +163,13 @@ class Realm(models.Model):
COMMUNITY = 2 COMMUNITY = 2
org_type = models.PositiveSmallIntegerField(default=CORPORATE) # type: int org_type = models.PositiveSmallIntegerField(default=CORPORATE) # type: int
# This value is also being used in static/js/settings_bots.bot_creation_policy_values.
# On updating it here, update it there as well.
BOT_CREATION_EVERYONE = 1
BOT_CREATION_LIMIT_GENERIC_BOTS = 2
BOT_CREATION_ADMINS_ONLY = 3
bot_creation_policy = models.PositiveSmallIntegerField(default=BOT_CREATION_EVERYONE) # type: int
date_created = models.DateTimeField(default=timezone_now) # type: datetime.datetime date_created = models.DateTimeField(default=timezone_now) # type: datetime.datetime
notifications_stream = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE) # type: Optional[Stream] notifications_stream = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE) # type: Optional[Stream]
signup_notifications_stream = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE) # type: Optional[Stream] signup_notifications_stream = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE) # type: Optional[Stream]
@ -183,7 +189,7 @@ class Realm(models.Model):
add_emoji_by_admins_only=bool, add_emoji_by_admins_only=bool,
allow_edit_history=bool, allow_edit_history=bool,
allow_message_deleting=bool, allow_message_deleting=bool,
create_generic_bot_by_admins_only=bool, bot_creation_policy=int,
create_stream_by_admins_only=bool, create_stream_by_admins_only=bool,
default_language=Text, default_language=Text,
description=Text, description=Text,
@ -214,6 +220,12 @@ class Realm(models.Model):
DEFAULT_NOTIFICATION_STREAM_NAME = u'announce' DEFAULT_NOTIFICATION_STREAM_NAME = u'announce'
INITIAL_PRIVATE_STREAM_NAME = u'core team' INITIAL_PRIVATE_STREAM_NAME = u'core team'
BOT_CREATION_POLICY_TYPES = [
BOT_CREATION_EVERYONE,
BOT_CREATION_LIMIT_GENERIC_BOTS,
BOT_CREATION_ADMINS_ONLY,
]
def authentication_methods_dict(self) -> Dict[Text, bool]: def authentication_methods_dict(self) -> Dict[Text, bool]:
"""Returns the a mapping from authentication flags to their status, """Returns the a mapping from authentication flags to their status,
showing only those authentication flags that are supported on showing only those authentication flags that are supported on
@ -724,7 +736,8 @@ class UserProfile(AbstractBaseUser, PermissionsMixin):
def allowed_bot_types(self): def allowed_bot_types(self):
# type: () -> List[int] # type: () -> List[int]
allowed_bot_types = [] allowed_bot_types = []
if self.is_realm_admin or not self.realm.create_generic_bot_by_admins_only: if self.is_realm_admin or \
not self.realm.bot_creation_policy == Realm.BOT_CREATION_LIMIT_GENERIC_BOTS:
allowed_bot_types.append(UserProfile.DEFAULT_BOT) allowed_bot_types.append(UserProfile.DEFAULT_BOT)
allowed_bot_types += [ allowed_bot_types += [
UserProfile.INCOMING_WEBHOOK_BOT, UserProfile.INCOMING_WEBHOOK_BOT,

View File

@ -570,7 +570,7 @@ class BotTest(ZulipTestCase, UploadSerializeMixin):
self.assert_num_bots_equal(0) self.assert_num_bots_equal(0)
self.assert_json_error(result, 'Invalid bot type') self.assert_json_error(result, 'Invalid bot type')
def test_add_bot_with_bot_type_not_allowed(self) -> None: def test_no_generic_bots_allowed_for_non_admins(self) -> None:
bot_info = { bot_info = {
'full_name': 'The Bot of Hamlet', 'full_name': 'The Bot of Hamlet',
'short_name': 'hambot', 'short_name': 'hambot',
@ -578,15 +578,15 @@ class BotTest(ZulipTestCase, UploadSerializeMixin):
} }
bot_email = 'hambot-bot@zulip.testserver' bot_email = 'hambot-bot@zulip.testserver'
bot_realm = get_realm('zulip') bot_realm = get_realm('zulip')
bot_realm.create_generic_bot_by_admins_only = True bot_realm.bot_creation_policy = Realm.BOT_CREATION_LIMIT_GENERIC_BOTS
bot_realm.save(update_fields=['create_generic_bot_by_admins_only']) bot_realm.save(update_fields=['bot_creation_policy'])
# A regular user cannot create a generic bot # A regular user cannot create a generic bot
self.login(self.example_email('hamlet')) self.login(self.example_email('hamlet'))
self.assert_num_bots_equal(0) self.assert_num_bots_equal(0)
result = self.client_post("/json/bots", bot_info) result = self.client_post("/json/bots", bot_info)
self.assert_num_bots_equal(0) self.assert_num_bots_equal(0)
self.assert_json_error(result, 'Invalid bot type') self.assert_json_error(result, 'Must be an organization administrator')
# But can create an incoming webhook # But can create an incoming webhook
self.assert_num_bots_equal(0) self.assert_num_bots_equal(0)
@ -595,11 +595,50 @@ class BotTest(ZulipTestCase, UploadSerializeMixin):
profile = get_user(bot_email, bot_realm) profile = get_user(bot_email, bot_realm)
self.assertEqual(profile.bot_type, UserProfile.INCOMING_WEBHOOK_BOT) self.assertEqual(profile.bot_type, UserProfile.INCOMING_WEBHOOK_BOT)
def test_add_bot_with_bot_type_not_allowed_admin(self) -> None: def test_no_generic_bots_allowed_for_admins(self) -> None:
bot_email = 'hambot-bot@zulip.testserver' bot_email = 'hambot-bot@zulip.testserver'
bot_realm = get_realm('zulip') bot_realm = get_realm('zulip')
bot_realm.create_generic_bot_by_admins_only = True bot_realm.bot_creation_policy = Realm.BOT_CREATION_LIMIT_GENERIC_BOTS
bot_realm.save(update_fields=['create_generic_bot_by_admins_only']) bot_realm.save(update_fields=['bot_creation_policy'])
# An administrator can create any type of bot
self.login(self.example_email('iago'))
self.assert_num_bots_equal(0)
self.create_bot(bot_type=UserProfile.DEFAULT_BOT)
self.assert_num_bots_equal(1)
profile = get_user(bot_email, bot_realm)
self.assertEqual(profile.bot_type, UserProfile.DEFAULT_BOT)
def test_no_bots_allowed_for_non_admins(self) -> None:
bot_info = {
'full_name': 'The Bot of Hamlet',
'short_name': 'hambot',
'bot_type': 1,
}
bot_realm = get_realm('zulip')
bot_realm.bot_creation_policy = Realm.BOT_CREATION_ADMINS_ONLY
bot_realm.save(update_fields=['bot_creation_policy'])
# A regular user cannot create a generic bot
self.login(self.example_email('hamlet'))
self.assert_num_bots_equal(0)
result = self.client_post("/json/bots", bot_info)
self.assert_num_bots_equal(0)
self.assert_json_error(result, 'Must be an organization administrator')
# Also, a regular user cannot create a incoming bot
bot_info['bot_type'] = 2
self.login(self.example_email('hamlet'))
self.assert_num_bots_equal(0)
result = self.client_post("/json/bots", bot_info)
self.assert_num_bots_equal(0)
self.assert_json_error(result, 'Must be an organization administrator')
def test_no_bots_allowed_for_admins(self) -> None:
bot_email = 'hambot-bot@zulip.testserver'
bot_realm = get_realm('zulip')
bot_realm.bot_creation_policy = Realm.BOT_CREATION_ADMINS_ONLY
bot_realm.save(update_fields=['bot_creation_policy'])
# An administrator can create any type of bot # An administrator can create any type of bot
self.login(self.example_email('iago')) self.login(self.example_email('iago'))

View File

@ -1231,6 +1231,7 @@ class EventsRegisterTest(ZulipTestCase):
message_retention_days=[10, 20], message_retention_days=[10, 20],
name=[u'Zulip', u'New Name'], name=[u'Zulip', u'New Name'],
waiting_period_threshold=[10, 20], waiting_period_threshold=[10, 20],
bot_creation_policy=[Realm.BOT_CREATION_EVERYONE],
) # type: Dict[str, Any] ) # type: Dict[str, Any]
vals = test_values.get(name) vals = test_values.get(name)

View File

@ -109,9 +109,9 @@ class HomeTest(ZulipTestCase):
"realm_allow_message_deleting", "realm_allow_message_deleting",
"realm_allow_message_editing", "realm_allow_message_editing",
"realm_authentication_methods", "realm_authentication_methods",
"realm_bot_creation_policy",
"realm_bot_domain", "realm_bot_domain",
"realm_bots", "realm_bots",
"realm_create_generic_bot_by_admins_only",
"realm_create_stream_by_admins_only", "realm_create_stream_by_admins_only",
"realm_default_language", "realm_default_language",
"realm_default_stream_groups", "realm_default_stream_groups",

View File

@ -295,6 +295,19 @@ class RealmTest(ZulipTestCase):
realm = get_realm('zulip') realm = get_realm('zulip')
self.assertFalse(realm.deactivated) self.assertFalse(realm.deactivated)
def test_change_bot_creation_policy(self) -> None:
# We need an admin user.
email = 'iago@zulip.com'
self.login(email)
req = dict(bot_creation_policy = ujson.dumps(Realm.BOT_CREATION_LIMIT_GENERIC_BOTS))
result = self.client_patch('/json/realm', req)
self.assert_json_success(result)
invalid_add_bot_permission = 4
req = dict(bot_creation_policy = ujson.dumps(invalid_add_bot_permission))
result = self.client_patch('/json/realm', req)
self.assert_json_error(result, 'Invalid bot creation policy')
class RealmAPITest(ZulipTestCase): class RealmAPITest(ZulipTestCase):
@ -329,6 +342,7 @@ class RealmAPITest(ZulipTestCase):
message_retention_days=[10, 20], message_retention_days=[10, 20],
name=[u'Zulip', u'New Name'], name=[u'Zulip', u'New Name'],
waiting_period_threshold=[10, 20], waiting_period_threshold=[10, 20],
bot_creation_policy=[1, 2],
) # type: Dict[str, Any] ) # type: Dict[str, Any]
vals = test_values.get(name) vals = test_values.get(name)
if Realm.property_types[name] is bool: if Realm.property_types[name] is bool:

View File

@ -38,7 +38,6 @@ def update_realm(
inline_url_embed_preview: Optional[bool]=REQ(validator=check_bool, default=None), inline_url_embed_preview: Optional[bool]=REQ(validator=check_bool, default=None),
create_stream_by_admins_only: Optional[bool]=REQ(validator=check_bool, default=None), create_stream_by_admins_only: Optional[bool]=REQ(validator=check_bool, default=None),
add_emoji_by_admins_only: Optional[bool]=REQ(validator=check_bool, default=None), add_emoji_by_admins_only: Optional[bool]=REQ(validator=check_bool, default=None),
create_generic_bot_by_admins_only: Optional[bool]=REQ(validator=check_bool, default=None),
allow_message_deleting: Optional[bool]=REQ(validator=check_bool, default=None), allow_message_deleting: Optional[bool]=REQ(validator=check_bool, default=None),
allow_message_editing: Optional[bool]=REQ(validator=check_bool, default=None), allow_message_editing: Optional[bool]=REQ(validator=check_bool, default=None),
mandatory_topics: Optional[bool]=REQ(validator=check_bool, default=None), mandatory_topics: Optional[bool]=REQ(validator=check_bool, default=None),
@ -50,7 +49,8 @@ def update_realm(
notifications_stream_id: Optional[int]=REQ(validator=check_int, default=None), notifications_stream_id: Optional[int]=REQ(validator=check_int, default=None),
signup_notifications_stream_id: Optional[int]=REQ(validator=check_int, default=None), signup_notifications_stream_id: Optional[int]=REQ(validator=check_int, default=None),
message_retention_days: Optional[int]=REQ(converter=to_not_negative_int_or_none, default=None), message_retention_days: Optional[int]=REQ(converter=to_not_negative_int_or_none, default=None),
send_welcome_emails: Optional[bool]=REQ(validator=check_bool, default=None) send_welcome_emails: Optional[bool]=REQ(validator=check_bool, default=None),
bot_creation_policy: Optional[int]=REQ(converter=to_not_negative_int_or_none, default=None)
) -> HttpResponse: ) -> HttpResponse:
realm = user_profile.realm realm = user_profile.realm
@ -67,6 +67,9 @@ def update_realm(
if signup_notifications_stream_id is not None and settings.NEW_USER_BOT is None: if signup_notifications_stream_id is not None and settings.NEW_USER_BOT is None:
return json_error(_("NEW_USER_BOT must configured first.")) return json_error(_("NEW_USER_BOT must configured first."))
# Additional validation of permissions values to add new bot
if bot_creation_policy is not None and bot_creation_policy not in Realm.BOT_CREATION_POLICY_TYPES:
return json_error(_("Invalid bot creation policy"))
# The user of `locals()` here is a bit of a code smell, but it's # The user of `locals()` here is a bit of a code smell, but it's
# restricted to the elements present in realm.property_types. # restricted to the elements present in realm.property_types.
# #

View File

@ -26,7 +26,7 @@ from zerver.lib.response import json_error, json_success
from zerver.lib.streams import access_stream_by_name from zerver.lib.streams import access_stream_by_name
from zerver.lib.upload import upload_avatar_image from zerver.lib.upload import upload_avatar_image
from zerver.lib.validator import check_bool, check_string, check_int, check_url, check_dict from zerver.lib.validator import check_bool, check_string, check_int, check_url, check_dict
from zerver.lib.users import check_valid_bot_type, \ from zerver.lib.users import check_valid_bot_type, check_bot_creation_policy, \
check_full_name, check_short_name, check_valid_interface_type, check_valid_bot_config check_full_name, check_short_name, check_valid_interface_type, check_valid_bot_config
from zerver.lib.utils import generate_random_token from zerver.lib.utils import generate_random_token
from zerver.models import UserProfile, Stream, Message, email_allowed_for_realm, \ from zerver.models import UserProfile, Stream, Message, email_allowed_for_realm, \
@ -299,6 +299,7 @@ def add_bot_backend(
return json_error(_("Username already in use")) return json_error(_("Username already in use"))
except UserProfile.DoesNotExist: except UserProfile.DoesNotExist:
pass pass
check_bot_creation_policy(user_profile, bot_type)
check_valid_bot_type(user_profile, bot_type) check_valid_bot_type(user_profile, bot_type)
check_valid_interface_type(interface_type) check_valid_interface_type(interface_type)