settings: Add setting for who can edit user groups.

Fixes #12380.
This commit is contained in:
Matheus Melo 2019-11-02 13:58:55 -03:00 committed by Tim Abbott
parent 849c925dc9
commit c96762b7a9
16 changed files with 217 additions and 22 deletions

View File

@ -59,6 +59,28 @@ run_test('can_edit', () => {
return false;
};
assert(!settings_user_groups.can_edit(1));
page_params.realm_user_group_edit_policy = 2;
page_params.is_admin = true;
assert(settings_user_groups.can_edit(1));
page_params.is_admin = false;
user_groups.is_member_of = (group_id, user_id) => {
assert.equal(group_id, 1);
assert.equal(user_id, undefined);
return true;
};
assert(!settings_user_groups.can_edit(1));
page_params.realm_user_group_edit_policy = 1;
page_params.is_admin = false;
user_groups.is_member_of = (group_id, user_id) => {
assert.equal(group_id, 1);
assert.equal(user_id, undefined);
return true;
};
assert(settings_user_groups.can_edit(1));
});
var user_group_selector = "#user-groups #1";

View File

@ -32,6 +32,8 @@ exports.build_page = function () {
realm_authentication_methods: page_params.realm_authentication_methods,
realm_create_stream_policy: page_params.realm_create_stream_policy,
realm_invite_to_stream_policy: page_params.realm_invite_to_stream_policy,
realm_user_group_edit_policy: page_params.realm_user_group_edit_policy,
USER_GROUP_EDIT_POLICY_MEMBERS: 1,
realm_name_changes_disabled: page_params.realm_name_changes_disabled,
realm_email_changes_disabled: page_params.realm_email_changes_disabled,
realm_avatar_changes_disabled: page_params.realm_avatar_changes_disabled,

View File

@ -88,6 +88,7 @@ exports.dispatch_normal_event = function dispatch_normal_event(event) {
allow_message_deleting: noop,
allow_message_editing: noop,
allow_community_topic_editing: noop,
user_group_edit_policy: noop,
avatar_changes_disabled: settings_account.update_avatar_change_display,
bot_creation_policy: settings_bots.update_bot_permissions_ui,
create_stream_policy: noop,

View File

@ -117,6 +117,15 @@ function get_property_value(property_name) {
}
}
if (property_name === 'realm_user_group_edit_policy') {
if (page_params.realm_user_group_edit_policy === 1) {
return "by_members";
}
if (page_params.realm_user_group_edit_policy === 2) {
return "by_admins_only";
}
}
if (property_name === 'realm_add_emoji_by_admins_only') {
if (page_params.realm_add_emoji_by_admins_only) {
return "by_admins_only";
@ -202,6 +211,11 @@ function set_invite_to_stream_policy_dropdown() {
$("#id_realm_invite_to_stream_policy").val(value);
}
function set_user_group_edit_policy_dropdown() {
var value = get_property_value("realm_user_group_edit_policy");
$("#id_realm_user_group_edit_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"));
}
@ -454,6 +468,8 @@ function update_dependent_subsettings(property_name) {
set_create_stream_policy_dropdown();
} else if (property_name === 'realm_invite_to_stream_policy') {
set_invite_to_stream_policy_dropdown();
} else if (property_name === 'realm_user_group_edit_policy') {
set_user_group_edit_policy_dropdown();
} else if (property_name === 'realm_video_chat_provider' ||
property_name === 'realm_google_hangouts_domain' ||
property_name.startsWith('realm_zoom')) {
@ -618,6 +634,7 @@ exports.build_page = function () {
set_user_invite_restriction_dropdown();
set_message_content_in_email_notifications_visiblity();
set_digest_emails_weekday_visibility();
set_user_group_edit_policy_dropdown();
function get_auth_method_table_data() {
const new_auth_methods = {};
@ -752,6 +769,7 @@ exports.build_page = function () {
const waiting_period_threshold = $("#id_realm_waiting_period_setting").val();
const create_stream_policy = $("#id_realm_create_stream_policy").val();
const invite_to_stream_policy = $("#id_realm_invite_to_stream_policy").val();
const user_group_edit_policy = $("#id_realm_user_group_edit_policy").val();
const add_emoji_permission = $("#id_realm_add_emoji_by_admins_only").val();
if (add_emoji_permission === "by_admins_only") {
@ -776,6 +794,12 @@ exports.build_page = function () {
data.invite_to_stream_policy = 3;
}
if (user_group_edit_policy === "by_admins_only") {
data.user_group_edit_policy = 2;
} else if (user_group_edit_policy === "by_members") {
data.user_group_edit_policy = 1;
}
if (waiting_period_threshold === "none") {
data.waiting_period_threshold = 0;
} else if (waiting_period_threshold === "three_days") {

View File

@ -19,6 +19,8 @@ exports.reload = function () {
exports.populate_user_groups();
};
const USER_GROUP_EDIT_POLICY_MEMBERS = 1;
exports.can_edit = function (group_id) {
if (page_params.is_admin) {
return true;
@ -28,6 +30,10 @@ exports.can_edit = function (group_id) {
return false;
}
if (page_params.realm_user_group_edit_policy !== USER_GROUP_EDIT_POLICY_MEMBERS) {
return false;
}
return user_groups.is_member_of(group_id, people.my_current_user_id());
};

View File

@ -114,6 +114,14 @@
</select>
</div>
<div class="input-group">
<label for="realm_user_group_edit_policy" class="dropdown-title">{{t "Who can create and manage user groups" }}</label>
<select name="realm_user_group_edit_policy" id="id_realm_user_group_edit_policy" class="prop-element">
<option value="by_admins_only">{{t "Admins" }}</option>
<option value="by_members">{{t "Admins and members" }}</option>
</select>
</div>
<div class="input-group">
<label for="realm_add_emoji_by_admins_only" class="dropdown-title">{{t "Who can add custom emoji" }}</label>
<select name="realm_add_emoji_by_admins_only" id="id_realm_add_emoji_by_admins_only" class="prop-element">

View File

@ -1,30 +1,37 @@
<div id="user-groups-admin" class="settings-section" data-name="user-groups-admin">
{{#unless is_admin}}
<div class="tip">Only group members and organization administrators can modify a group.</div>
{{#if (eq realm_user_group_edit_policy USER_GROUP_EDIT_POLICY_MEMBERS) }}
<div class="tip">{{t 'Only group members and organization administrators can modify a group.' }}</div>
{{else}}
<div class="tip">{{t 'Only organization administrators can modify user groups in this organization.' }}</div>
{{/if}}
{{/unless}}
{{#unless is_guest}}
<p>
{{#tr this}}User groups allow you to <a href="/help/mention-a-user-or-group" target="_blank">mention</a> multiple users at once. When you mention a user group, everyone in the group is notified as if they were individually mentioned.{{/tr}}
</p>
<form class="form-horizontal admin-user-group-form">
<div class="add-new-user-group-box grey-box">
<div class="new-user-group-form">
<div class="settings-section-title new-user-group-section-title no-padding">{{t "Add a new user group" }}</div>
<div class="alert" id="admin-user-group-status"></div>
<div class="inline-block">
<label for="user_group_name">{{t "Name" }}</label>
<input type="text" name="name" id="user_group_name" maxlength="100" placeholder="{{t 'marketing' }}" />
<p>
{{#tr this}}User groups allow you to <a href="/help/mention-a-user-or-group" target="_blank">mention</a> multiple users at once. When you mention a user group, everyone in the group is notified as if they were individually mentioned.{{/tr}}
</p>
{{#unless (and (not (eq realm_user_group_edit_policy USER_GROUP_EDIT_POLICY_MEMBERS) (not is_admin)))}}
<form class="form-horizontal admin-user-group-form">
<div class="add-new-user-group-box grey-box">
<div class="new-user-group-form">
<div class="settings-section-title new-user-group-section-title no-padding">{{t "Add a new user group" }}</div>
<div class="alert" id="admin-user-group-status"></div>
<div class="inline-block">
<label for="user_group_name">{{t "Name" }}</label>
<input type="text" name="name" id="user_group_name" maxlength="100" placeholder="{{t 'marketing' }}" />
</div>
<div class="inline-block">
<label for="user_group_description">{{t "Description" }}</label>
<input type="text" name="description" id="user_group_description" maxlength="300" placeholder="{{t 'Marketing team' }}" />
</div>
<button type="submit" class="button rounded sea-green">
{{t 'Save' }}
</button>
</div>
<div class="inline-block">
<label for="user_group_description">{{t "Description" }}</label>
<input type="text" name="description" id="user_group_description" maxlength="300" placeholder="{{t 'Marketing team' }}" />
</div>
<button type="submit" class="button rounded sea-green">
{{t 'Save' }}
</button>
</div>
</div>
</form>
</form>
{{/unless}}
{{/unless}}
<div id="user-groups" class="new-style"></div>

View File

@ -533,6 +533,17 @@ def require_member_or_admin(view_func: ViewFuncT) -> ViewFuncT:
return view_func(request, user_profile, *args, **kwargs)
return _wrapped_view_func # type: ignore # https://github.com/python/mypy/issues/1927
def require_user_group_edit_policy(view_func: ViewFuncT) -> ViewFuncT:
@wraps(view_func)
def _wrapped_view_func(request: HttpRequest, user_profile: UserProfile,
*args: Any, **kwargs: Any) -> HttpResponse:
realm = user_profile.realm
if realm.user_group_edit_policy != Realm.USER_GROUP_EDIT_POLICY_MEMBERS and \
not user_profile.is_realm_admin:
raise JsonableError(_("Must be an organization administrator"))
return view_func(request, user_profile, *args, **kwargs)
return _wrapped_view_func # type: ignore # https://github.com/python/mypy/issues/1927
# This API endpoint is used only for the mobile apps. It is part of a
# workaround for the fact that React Native doesn't support setting
# HTTP basic authentication headers.

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.24 on 2019-10-16 22:48
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('zerver', '0251_prereg_user_add_full_name'),
]
operations = [
migrations.AddField(
model_name='realm',
name='user_group_edit_policy',
field=models.PositiveSmallIntegerField(default=1),
),
]

View File

@ -197,6 +197,11 @@ class Realm(models.Model):
invite_to_stream_policy = models.PositiveSmallIntegerField(
default=INVITE_TO_STREAM_POLICY_MEMBERS) # type: int
USER_GROUP_EDIT_POLICY_MEMBERS = 1
USER_GROUP_EDIT_POLICY_ADMINS = 2
user_group_edit_policy = models.PositiveSmallIntegerField(
default=INVITE_TO_STREAM_POLICY_MEMBERS) # type: int
# 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.
@ -330,6 +335,7 @@ class Realm(models.Model):
video_chat_provider=int,
waiting_period_threshold=int,
digest_weekday=int,
user_group_edit_policy=int,
) # type: Dict[str, Union[type, Tuple[type, ...]]]
# Icon is the square mobile icon.

View File

@ -1588,6 +1588,7 @@ class EventsRegisterTest(ZulipTestCase):
waiting_period_threshold=[10, 20],
create_stream_policy=[3, 2, 1],
invite_to_stream_policy=[3, 2, 1],
user_group_edit_policy=[1, 2],
email_address_visibility=[Realm.EMAIL_ADDRESS_VISIBILITY_ADMINS],
bot_creation_policy=[Realm.BOT_CREATION_EVERYONE],
video_chat_provider=[

View File

@ -180,6 +180,7 @@ class HomeTest(ZulipTestCase):
"realm_signup_notifications_stream_id",
"realm_upload_quota",
"realm_uri",
"realm_user_group_edit_policy",
"realm_user_groups",
"realm_users",
"realm_video_chat_provider",

View File

@ -591,6 +591,8 @@ class RealmAPITest(ZulipTestCase):
create_stream_policy=[Realm.CREATE_STREAM_POLICY_ADMINS,
Realm.CREATE_STREAM_POLICY_MEMBERS,
Realm.CREATE_STREAM_POLICY_WAITING_PERIOD],
user_group_edit_policy=[Realm.USER_GROUP_EDIT_POLICY_ADMINS,
Realm.USER_GROUP_EDIT_POLICY_MEMBERS],
invite_to_stream_policy=[Realm.INVITE_TO_STREAM_POLICY_ADMINS,
Realm.INVITE_TO_STREAM_POLICY_MEMBERS,
Realm.INVITE_TO_STREAM_POLICY_WAITING_PERIOD],

View File

@ -10,6 +10,7 @@ from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.test_helpers import (
most_recent_usermessage,
)
from zerver.lib.actions import do_set_realm_property
from zerver.lib.user_groups import (
check_add_user_to_user_group,
check_remove_user_from_user_group,
@ -440,3 +441,81 @@ class UserGroupAPITestCase(ZulipTestCase):
for user in other_users:
um = most_recent_usermessage(user)
self.assertFalse(um.flags.mentioned)
def test_only_admin_manage_groups(self) -> None:
iago = self.example_user('iago')
hamlet = self.example_user('hamlet')
cordelia = self.example_user('cordelia')
self.login(iago.email)
do_set_realm_property(iago.realm, 'user_group_edit_policy',
Realm.USER_GROUP_EDIT_POLICY_ADMINS)
params = {
'name': 'support',
'members': ujson.dumps([iago.id, hamlet.id]),
'description': 'Support team',
}
result = self.client_post('/json/user_groups/create', info=params)
self.assert_json_success(result)
user_group = UserGroup.objects.get(name='support')
# Test add member
params = {'add': ujson.dumps([cordelia.id])}
result = self.client_post('/json/user_groups/{}/members'.format(user_group.id),
info=params)
self.assert_json_success(result)
# Test remove member
params = {'delete': ujson.dumps([cordelia.id])}
result = self.client_post('/json/user_groups/{}/members'.format(user_group.id),
info=params)
self.assert_json_success(result)
# Test changing groups name
params = {
'name': 'help',
'description': 'Troubleshooting',
}
result = self.client_patch('/json/user_groups/{}'.format(user_group.id), info=params)
self.assert_json_success(result)
# Test delete a group
result = self.client_delete('/json/user_groups/{}'.format(user_group.id))
self.assert_json_success(result)
user_group = create_user_group(name='support',
members=[hamlet, iago],
realm=iago.realm,
)
self.logout()
self.login(self.example_email("hamlet"))
# Test creating a group
params = {
'name': 'support2',
'members': ujson.dumps([hamlet.id]),
'description': 'Support team',
}
result = self.client_post('/json/user_groups/create', info=params)
self.assert_json_error(result, "Must be an organization administrator")
# Test add member
params = {'add': ujson.dumps([cordelia.id])}
result = self.client_post('/json/user_groups/{}/members'.format(user_group.id),
info=params)
self.assert_json_error(result, "Must be an organization administrator")
# Test delete a group
result = self.client_delete('/json/user_groups/{}'.format(user_group.id))
self.assert_json_error(result, "Must be an organization administrator")
# Test changing groups name
params = {
'name': 'help',
'description': 'Troubleshooting',
}
result = self.client_patch('/json/user_groups/{}'.format(user_group.id), info=params)
self.assert_json_error(result, "Must be an organization administrator")

View File

@ -63,6 +63,7 @@ def update_realm(
bot_creation_policy: Optional[int]=REQ(converter=to_not_negative_int_or_none, default=None),
create_stream_policy: Optional[int]=REQ(validator=check_int, default=None),
invite_to_stream_policy: Optional[int]=REQ(validator=check_int, default=None),
user_group_edit_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[int]=REQ(validator=check_int, default=None),

View File

@ -3,7 +3,7 @@ from django.utils.translation import ugettext as _
from typing import List
from zerver.decorator import require_member_or_admin
from zerver.decorator import require_member_or_admin, require_user_group_edit_policy
from zerver.lib.actions import check_add_user_group, do_update_user_group_name, \
do_update_user_group_description, bulk_add_members_to_user_group, \
remove_members_from_user_group, check_delete_user_group
@ -18,6 +18,7 @@ from zerver.models import UserProfile
from zerver.views.streams import compose_views, FuncKwargPair
@require_member_or_admin
@require_user_group_edit_policy
@has_request_variables
def add_user_group(request: HttpRequest, user_profile: UserProfile,
name: str=REQ(),
@ -34,6 +35,7 @@ def get_user_group(request: HttpRequest, user_profile: UserProfile) -> HttpRespo
return json_success({"user_groups": user_groups})
@require_member_or_admin
@require_user_group_edit_policy
@has_request_variables
def edit_user_group(request: HttpRequest, user_profile: UserProfile,
user_group_id: int=REQ(validator=check_int),
@ -53,6 +55,7 @@ def edit_user_group(request: HttpRequest, user_profile: UserProfile,
return json_success()
@require_member_or_admin
@require_user_group_edit_policy
@has_request_variables
def delete_user_group(request: HttpRequest, user_profile: UserProfile,
user_group_id: int=REQ(validator=check_int)) -> HttpResponse:
@ -61,6 +64,7 @@ def delete_user_group(request: HttpRequest, user_profile: UserProfile,
return json_success()
@require_member_or_admin
@require_user_group_edit_policy
@has_request_variables
def update_user_group_backend(request: HttpRequest, user_profile: UserProfile,
user_group_id: int=REQ(validator=check_int),