mirror of https://github.com/zulip/zulip.git
users: Modify update user API endpoint to accept role as parameter.
This commit changes the update user API endpoint to accept role as parameter instead of the bool parameters is_guest and is_admin. User role dropdown in user info modal is also modified to use "dropdown_options_widget". Modified by tabbott to document the API change.
This commit is contained in:
parent
74c61984df
commit
9fa60672e6
|
@ -158,6 +158,21 @@ exports.msg_delete_limit_dropdown_values = time_limit_dropdown_values;
|
||||||
|
|
||||||
exports.retain_message_forever = -1;
|
exports.retain_message_forever = -1;
|
||||||
|
|
||||||
|
exports.user_role_values = {
|
||||||
|
guest: {
|
||||||
|
code: 600,
|
||||||
|
description: i18n.t("Guest"),
|
||||||
|
},
|
||||||
|
member: {
|
||||||
|
code: 400,
|
||||||
|
description: i18n.t("Member"),
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
code: 200,
|
||||||
|
description: i18n.t("Administrator"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// NOTIFICATIONS
|
// NOTIFICATIONS
|
||||||
|
|
||||||
exports.general_notifications_table_labels = {
|
exports.general_notifications_table_labels = {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const settings_data = require("./settings_data");
|
const settings_data = require("./settings_data");
|
||||||
|
const settings_config = require("./settings_config");
|
||||||
const render_admin_user_list = require("../templates/admin_user_list.hbs");
|
const render_admin_user_list = require("../templates/admin_user_list.hbs");
|
||||||
const render_admin_human_form = require('../templates/admin_human_form.hbs');
|
const render_admin_human_form = require('../templates/admin_human_form.hbs');
|
||||||
const render_admin_bot_form = require('../templates/admin_bot_form.hbs');
|
const render_admin_bot_form = require('../templates/admin_bot_form.hbs');
|
||||||
|
@ -66,6 +67,16 @@ function get_user_info_row(user_id) {
|
||||||
return $("tr.user_row[data-user-id='" + user_id + "']");
|
return $("tr.user_row[data-user-id='" + user_id + "']");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function set_user_role_dropdown(person) {
|
||||||
|
let role_value = settings_config.user_role_values.member.code;
|
||||||
|
if (person.is_admin) {
|
||||||
|
role_value = settings_config.user_role_values.admin.code;
|
||||||
|
} else if (person.is_guest) {
|
||||||
|
role_value = settings_config.user_role_values.guest.code;
|
||||||
|
}
|
||||||
|
$('#user-role-select').val(role_value);
|
||||||
|
}
|
||||||
|
|
||||||
function update_view_on_deactivate(row) {
|
function update_view_on_deactivate(row) {
|
||||||
const button = row.find("button.deactivate");
|
const button = row.find("button.deactivate");
|
||||||
const user_role = row.find(".user_role");
|
const user_role = row.find(".user_role");
|
||||||
|
@ -377,14 +388,13 @@ function open_human_form(person) {
|
||||||
user_id: user_id,
|
user_id: user_id,
|
||||||
email: person.email,
|
email: person.email,
|
||||||
full_name: person.full_name,
|
full_name: person.full_name,
|
||||||
is_admin: person.is_admin,
|
user_role_values: settings_config.user_role_values,
|
||||||
is_guest: person.is_guest,
|
|
||||||
is_member: !person.is_admin && !person.is_guest,
|
|
||||||
});
|
});
|
||||||
const div = $(html);
|
const div = $(html);
|
||||||
const modal_container = $('#user-info-form-modal-container');
|
const modal_container = $('#user-info-form-modal-container');
|
||||||
modal_container.empty().append(div);
|
modal_container.empty().append(div);
|
||||||
overlays.open_modal('#admin-human-form');
|
overlays.open_modal('#admin-human-form');
|
||||||
|
set_user_role_dropdown(person);
|
||||||
|
|
||||||
const element = "#admin-human-form .custom-profile-field-form";
|
const element = "#admin-human-form .custom-profile-field-form";
|
||||||
$(element).html("");
|
$(element).html("");
|
||||||
|
@ -587,15 +597,14 @@ function handle_human_form(tbody, status_field) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
const user_role_select_value = modal.find('#user-role-select').val();
|
const role = parseInt(modal.find('#user-role-select').val().trim(), 10);
|
||||||
const full_name = modal.find("input[name='full_name']");
|
const full_name = modal.find("input[name='full_name']");
|
||||||
const profile_data = get_human_profile_data(fields_user_pills);
|
const profile_data = get_human_profile_data(fields_user_pills);
|
||||||
|
|
||||||
const url = "/json/users/" + encodeURIComponent(user_id);
|
const url = "/json/users/" + encodeURIComponent(user_id);
|
||||||
const data = {
|
const data = {
|
||||||
full_name: JSON.stringify(full_name.val()),
|
full_name: JSON.stringify(full_name.val()),
|
||||||
is_admin: JSON.stringify(user_role_select_value === 'admin'),
|
role: JSON.stringify(role),
|
||||||
is_guest: JSON.stringify(user_role_select_value === 'guest'),
|
|
||||||
profile_data: JSON.stringify(profile_data),
|
profile_data: JSON.stringify(profile_data),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -17,10 +17,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label class="input-label" for="user-role-select">{{t 'User role' }}</label>
|
<label class="input-label" for="user-role-select">{{t 'User role' }}</label>
|
||||||
<select name="user-role-select" id="user-role-select">
|
<select name="user-role-select" id="user-role-select" data-setting-widget-type="number">
|
||||||
<option value="guest" {{#if is_guest}}selected{{/if}}>{{t 'Guest'}}</option>
|
{{> settings/dropdown_options_widget option_values=user_role_values}}
|
||||||
<option value="regular" {{#if is_member}}selected{{/if}}>{{t 'Member' }}</option>
|
|
||||||
<option value="admin" {{#if is_admin}}selected{{/if}}>{{t 'Administrator' }}</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-profile-field-form"></div>
|
<div class="custom-profile-field-form"></div>
|
||||||
|
|
|
@ -16,6 +16,9 @@ below features are supported.
|
||||||
the user; use the `user_id` field instead. Previously, some (but
|
the user; use the `user_id` field instead. Previously, some (but
|
||||||
not all) events of these types contained an `email` key in addition to
|
not all) events of these types contained an `email` key in addition to
|
||||||
to `user_id`) for identifying the modified user.
|
to `user_id`) for identifying the modified user.
|
||||||
|
* [`PATCH /users/{user_id}`](/api/update-user): The `is_admin` and
|
||||||
|
`is_guest` parameters were removed in favor of the more general
|
||||||
|
`role` parameter for specifying a change in user role.
|
||||||
|
|
||||||
**Feature level 6**
|
**Feature level 6**
|
||||||
* [`GET /events`](/api/get-events-from-queue): `realm_user` events to
|
* [`GET /events`](/api/get-events-from-queue): `realm_user` events to
|
||||||
|
|
|
@ -880,6 +880,12 @@ class UserProfile(AbstractBaseUser, PermissionsMixin):
|
||||||
ROLE_GUEST = 600
|
ROLE_GUEST = 600
|
||||||
role: int = models.PositiveSmallIntegerField(default=ROLE_MEMBER, db_index=True)
|
role: int = models.PositiveSmallIntegerField(default=ROLE_MEMBER, db_index=True)
|
||||||
|
|
||||||
|
ROLE_TYPES = [
|
||||||
|
ROLE_REALM_ADMINISTRATOR,
|
||||||
|
ROLE_MEMBER,
|
||||||
|
ROLE_GUEST,
|
||||||
|
]
|
||||||
|
|
||||||
# Whether the user has been "soft-deactivated" due to weeks of inactivity.
|
# Whether the user has been "soft-deactivated" due to weeks of inactivity.
|
||||||
# For these users we avoid doing UserMessage table work, as an optimization
|
# For these users we avoid doing UserMessage table work, as an optimization
|
||||||
# for large Zulip organizations with lots of single-visit users.
|
# for large Zulip organizations with lots of single-visit users.
|
||||||
|
|
|
@ -1579,21 +1579,24 @@ paths:
|
||||||
type: string
|
type: string
|
||||||
example: NewName
|
example: NewName
|
||||||
required: false
|
required: false
|
||||||
- name: is_admin
|
- name: role
|
||||||
in: query
|
in: query
|
||||||
description: |
|
description: |
|
||||||
Whether the target user is an administrator.
|
New [role](/help/roles-and-permissions) for the user. Supported roles include:
|
||||||
|
* Organization owner: 100
|
||||||
|
* Organization administrator: 200
|
||||||
|
* Member: 400
|
||||||
|
* Guest: 600
|
||||||
|
|
||||||
|
Only organization owners can add or remove the owner permission.
|
||||||
|
|
||||||
|
The owner permission cannot be removed from the only organization owner.
|
||||||
|
|
||||||
|
**Changes**: New in Zulip 2.2 (feature level 8), replacing the previous
|
||||||
|
pair of `is_admin` and `is_guest` boolean parameters.
|
||||||
schema:
|
schema:
|
||||||
type: boolean
|
type: integer
|
||||||
example: false
|
example: 400
|
||||||
required: false
|
|
||||||
- name: is_guest
|
|
||||||
in: query
|
|
||||||
description: |
|
|
||||||
Whether the target user is a guest.
|
|
||||||
schema:
|
|
||||||
type: boolean
|
|
||||||
example: true
|
|
||||||
required: false
|
required: false
|
||||||
- name: profile_data
|
- name: profile_data
|
||||||
in: query
|
in: query
|
||||||
|
|
|
@ -138,7 +138,7 @@ class PermissionTest(ZulipTestCase):
|
||||||
self.assertFalse(othello_dict['is_admin'])
|
self.assertFalse(othello_dict['is_admin'])
|
||||||
|
|
||||||
# Giveth
|
# Giveth
|
||||||
req = dict(is_admin=ujson.dumps(True))
|
req = dict(role=ujson.dumps(UserProfile.ROLE_REALM_ADMINISTRATOR))
|
||||||
|
|
||||||
events: List[Mapping[str, Any]] = []
|
events: List[Mapping[str, Any]] = []
|
||||||
with tornado_redirected_to_list(events):
|
with tornado_redirected_to_list(events):
|
||||||
|
@ -151,7 +151,7 @@ class PermissionTest(ZulipTestCase):
|
||||||
self.assertEqual(person['is_admin'], True)
|
self.assertEqual(person['is_admin'], True)
|
||||||
|
|
||||||
# Taketh away
|
# Taketh away
|
||||||
req = dict(is_admin=ujson.dumps(False))
|
req = dict(role=ujson.dumps(UserProfile.ROLE_MEMBER))
|
||||||
events = []
|
events = []
|
||||||
with tornado_redirected_to_list(events):
|
with tornado_redirected_to_list(events):
|
||||||
result = self.client_patch('/json/users/{}'.format(othello.id), req)
|
result = self.client_patch('/json/users/{}'.format(othello.id), req)
|
||||||
|
@ -164,7 +164,7 @@ class PermissionTest(ZulipTestCase):
|
||||||
|
|
||||||
# Cannot take away from last admin
|
# Cannot take away from last admin
|
||||||
self.login('iago')
|
self.login('iago')
|
||||||
req = dict(is_admin=ujson.dumps(False))
|
req = dict(role=ujson.dumps(UserProfile.ROLE_MEMBER))
|
||||||
events = []
|
events = []
|
||||||
with tornado_redirected_to_list(events):
|
with tornado_redirected_to_list(events):
|
||||||
result = self.client_patch('/json/users/{}'.format(desdemona.id), req)
|
result = self.client_patch('/json/users/{}'.format(desdemona.id), req)
|
||||||
|
@ -355,15 +355,7 @@ class PermissionTest(ZulipTestCase):
|
||||||
hamlet = self.example_user("hamlet")
|
hamlet = self.example_user("hamlet")
|
||||||
self.assertFalse(hamlet.is_guest)
|
self.assertFalse(hamlet.is_guest)
|
||||||
|
|
||||||
# Test failure of making user both admin and guest
|
req = dict(role=ujson.dumps(UserProfile.ROLE_GUEST))
|
||||||
req = dict(is_guest=ujson.dumps(True), is_admin=ujson.dumps(True))
|
|
||||||
result = self.client_patch('/json/users/{}'.format(hamlet.id), req)
|
|
||||||
self.assert_json_error(result, 'Guests cannot be organization administrators')
|
|
||||||
self.assertFalse(hamlet.is_guest)
|
|
||||||
self.assertFalse(hamlet.is_realm_admin)
|
|
||||||
hamlet = self.example_user("hamlet")
|
|
||||||
|
|
||||||
req = dict(is_guest=ujson.dumps(True))
|
|
||||||
events: List[Mapping[str, Any]] = []
|
events: List[Mapping[str, Any]] = []
|
||||||
with tornado_redirected_to_list(events):
|
with tornado_redirected_to_list(events):
|
||||||
result = self.client_patch('/json/users/{}'.format(hamlet.id), req)
|
result = self.client_patch('/json/users/{}'.format(hamlet.id), req)
|
||||||
|
@ -382,7 +374,7 @@ class PermissionTest(ZulipTestCase):
|
||||||
|
|
||||||
polonius = self.example_user("polonius")
|
polonius = self.example_user("polonius")
|
||||||
self.assertTrue(polonius.is_guest)
|
self.assertTrue(polonius.is_guest)
|
||||||
req = dict(is_guest=ujson.dumps(False))
|
req = dict(role=ujson.dumps(UserProfile.ROLE_MEMBER))
|
||||||
events: List[Mapping[str, Any]] = []
|
events: List[Mapping[str, Any]] = []
|
||||||
with tornado_redirected_to_list(events):
|
with tornado_redirected_to_list(events):
|
||||||
result = self.client_patch('/json/users/{}'.format(polonius.id), req)
|
result = self.client_patch('/json/users/{}'.format(polonius.id), req)
|
||||||
|
@ -402,15 +394,10 @@ class PermissionTest(ZulipTestCase):
|
||||||
self.assertFalse(hamlet.is_guest)
|
self.assertFalse(hamlet.is_guest)
|
||||||
self.assertTrue(hamlet.is_realm_admin)
|
self.assertTrue(hamlet.is_realm_admin)
|
||||||
|
|
||||||
# Test failure of making a admin to guest without revoking admin status
|
|
||||||
req = dict(is_guest=ujson.dumps(True))
|
|
||||||
result = self.client_patch('/json/users/{}'.format(hamlet.id), req)
|
|
||||||
self.assert_json_error(result, 'Guests cannot be organization administrators')
|
|
||||||
|
|
||||||
# Test changing a user from admin to guest and revoking admin status
|
# Test changing a user from admin to guest and revoking admin status
|
||||||
hamlet = self.example_user("hamlet")
|
hamlet = self.example_user("hamlet")
|
||||||
self.assertFalse(hamlet.is_guest)
|
self.assertFalse(hamlet.is_guest)
|
||||||
req = dict(is_admin=ujson.dumps(False), is_guest=ujson.dumps(True))
|
req = dict(role=ujson.dumps(UserProfile.ROLE_GUEST))
|
||||||
events: List[Mapping[str, Any]] = []
|
events: List[Mapping[str, Any]] = []
|
||||||
with tornado_redirected_to_list(events):
|
with tornado_redirected_to_list(events):
|
||||||
result = self.client_patch('/json/users/{}'.format(hamlet.id), req)
|
result = self.client_patch('/json/users/{}'.format(hamlet.id), req)
|
||||||
|
@ -435,15 +422,10 @@ class PermissionTest(ZulipTestCase):
|
||||||
self.assertTrue(polonius.is_guest)
|
self.assertTrue(polonius.is_guest)
|
||||||
self.assertFalse(polonius.is_realm_admin)
|
self.assertFalse(polonius.is_realm_admin)
|
||||||
|
|
||||||
# Test failure of making a guest to admin without revoking guest status
|
|
||||||
req = dict(is_admin=ujson.dumps(True))
|
|
||||||
result = self.client_patch('/json/users/{}'.format(polonius.id), req)
|
|
||||||
self.assert_json_error(result, 'Guests cannot be organization administrators')
|
|
||||||
|
|
||||||
# Test changing a user from guest to admin and revoking guest status
|
# Test changing a user from guest to admin and revoking guest status
|
||||||
polonius = self.example_user("polonius")
|
polonius = self.example_user("polonius")
|
||||||
self.assertFalse(polonius.is_realm_admin)
|
self.assertFalse(polonius.is_realm_admin)
|
||||||
req = dict(is_admin=ujson.dumps(True), is_guest=ujson.dumps(False))
|
req = dict(role=ujson.dumps(UserProfile.ROLE_REALM_ADMINISTRATOR))
|
||||||
events: List[Mapping[str, Any]] = []
|
events: List[Mapping[str, Any]] = []
|
||||||
with tornado_redirected_to_list(events):
|
with tornado_redirected_to_list(events):
|
||||||
result = self.client_patch('/json/users/{}'.format(polonius.id), req)
|
result = self.client_patch('/json/users/{}'.format(polonius.id), req)
|
||||||
|
|
|
@ -24,7 +24,8 @@ from zerver.lib.request import has_request_variables, REQ
|
||||||
from zerver.lib.response import json_error, json_success
|
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, check_list
|
from zerver.lib.validator import check_bool, check_string, check_int, check_url, check_dict, check_list, \
|
||||||
|
check_int_in
|
||||||
from zerver.lib.url_encoding import add_query_arg_to_redirect_url
|
from zerver.lib.url_encoding import add_query_arg_to_redirect_url
|
||||||
from zerver.lib.users import check_valid_bot_type, check_bot_creation_policy, \
|
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, \
|
||||||
|
@ -80,36 +81,16 @@ def reactivate_user_backend(request: HttpRequest, user_profile: UserProfile,
|
||||||
@has_request_variables
|
@has_request_variables
|
||||||
def update_user_backend(request: HttpRequest, user_profile: UserProfile, user_id: int,
|
def update_user_backend(request: HttpRequest, user_profile: UserProfile, user_id: int,
|
||||||
full_name: Optional[str]=REQ(default="", validator=check_string),
|
full_name: Optional[str]=REQ(default="", validator=check_string),
|
||||||
is_admin: Optional[bool]=REQ(default=None, validator=check_bool),
|
role: Optional[int]=REQ(default=None, validator=check_int_in(
|
||||||
is_guest: Optional[bool]=REQ(default=None, validator=check_bool),
|
UserProfile.ROLE_TYPES)),
|
||||||
profile_data: Optional[List[Dict[str, Union[int, str, List[int]]]]]=
|
profile_data: Optional[List[Dict[str, Union[int, str, List[int]]]]]=
|
||||||
REQ(default=None,
|
REQ(default=None,
|
||||||
validator=check_list(check_dict([('id', check_int)])))) -> HttpResponse:
|
validator=check_list(check_dict([('id', check_int)])))) -> HttpResponse:
|
||||||
target = access_user_by_id(user_profile, user_id, allow_deactivated=True, allow_bots=True)
|
target = access_user_by_id(user_profile, user_id, allow_deactivated=True, allow_bots=True)
|
||||||
|
|
||||||
# Historically, UserProfile had two fields, is_guest and is_realm_admin.
|
|
||||||
# This condition protected against situations where update_user_backend
|
|
||||||
# could cause both is_guest and is_realm_admin to be set.
|
|
||||||
# Once we update the frontend to just send a 'role' value, we can remove this check.
|
|
||||||
if (((is_guest is None and target.is_guest) or is_guest) and
|
|
||||||
((is_admin is None and target.is_realm_admin) or is_admin)):
|
|
||||||
return json_error(_("Guests cannot be organization administrators"))
|
|
||||||
|
|
||||||
role = None
|
|
||||||
if is_admin is not None and target.is_realm_admin != is_admin:
|
|
||||||
if not is_admin and check_last_admin(user_profile):
|
|
||||||
return json_error(_('Cannot remove the only organization administrator'))
|
|
||||||
role = UserProfile.ROLE_MEMBER
|
|
||||||
if is_admin:
|
|
||||||
role = UserProfile.ROLE_REALM_ADMINISTRATOR
|
|
||||||
|
|
||||||
if is_guest is not None and target.is_guest != is_guest:
|
|
||||||
if is_guest:
|
|
||||||
role = UserProfile.ROLE_GUEST
|
|
||||||
if role is None:
|
|
||||||
role = UserProfile.ROLE_MEMBER
|
|
||||||
|
|
||||||
if role is not None and target.role != role:
|
if role is not None and target.role != role:
|
||||||
|
if target.role == UserProfile.ROLE_REALM_ADMINISTRATOR and check_last_admin(user_profile):
|
||||||
|
return json_error(_('Cannot remove the only organization administrator'))
|
||||||
do_change_user_role(target, role)
|
do_change_user_role(target, role)
|
||||||
|
|
||||||
if (full_name is not None and target.full_name != full_name and
|
if (full_name is not None and target.full_name != full_name and
|
||||||
|
|
Loading…
Reference in New Issue