admin: Enable admins to toggle supported auth methods via UI.

Add a table to the administration page that will allow realm admins to
activate and deactivate the supported authentication methods for that
realm.
This commit is contained in:
umkay 2016-11-02 13:51:56 -07:00 committed by Tim Abbott
parent 21c024fc29
commit f4c621ffe3
14 changed files with 235 additions and 10 deletions

View File

@ -529,6 +529,52 @@ casper.waitUntilVisible('#admin-realm-default-language-status', function () {
casper.test.assertSelectorHasText('#admin-realm-default-language-status', 'Default language changed!');
});
// Test authentication methods setting
casper.waitForSelector('input[type="checkbox"]', function () {
casper.click(".method_row[data-method='Email'] input[type='checkbox']");
casper.click('form.admin-realm-form input.button');
});
// Test setting was activated--default is checked
casper.then(function () {
// Scroll to bottom so that casper snapshots show the auth methods table
this.scrollToBottom();
// Test setting was activated
casper.waitUntilVisible('#admin-realm-authentication-methods-status', function () {
casper.test.assertSelectorHasText('#admin-realm-authentication-methods-status', 'Authentication methods saved!');
casper.test.assertEval(function () {
return !(document.querySelector(".method_row[data-method='Email'] input[type='checkbox']").checked);
});
});
});
casper.then(function () {
// Leave the page and return
casper.click('#settings-dropdown');
casper.click('a[href^="#subscriptions"]');
casper.click('#settings-dropdown');
casper.click('a[href^="#administration"]');
casper.waitForSelector(".method_row[data-method='Email'] input[type='checkbox']", function () {
// Test Setting was saved
casper.test.assertEval(function () {
return !(document.querySelector(".method_row[data-method='Email'] input[type='checkbox']").checked);
});
});
});
// Deactivate setting--default is checked
casper.then(function () {
casper.click(".method_row[data-method='Email'] input[type='checkbox']");
casper.click('form.admin-realm-form input.button');
casper.waitUntilVisible('#admin-realm-authentication-methods-status', function () {
casper.test.assertSelectorHasText('#admin-realm-authentication-methods-status', 'Authentication methods saved!');
casper.test.assertEval(function () {
return document.querySelector(".method_row[data-method='Email'] input[type='checkbox']").checked;
});
});
});
common.then_log_out();
casper.run(function () {

View File

@ -216,6 +216,26 @@ function render(template_name, args) {
assert.equal(count.text(), 99);
}());
(function admin_auth_methods_list() {
var args = {
method: {
"method": "Email",
"enabled": false
}
};
var html = '';
html += '<tbody id="admin_auth_methods_table">';
html += render('admin_auth_methods_list', args);
html += '</tbody>';
global.write_test_output('admin_auth_methods_list.handlebars', html);
var method = $(html).find('tr.method_row:first span.method');
assert.equal(method.text(), 'Email');
assert.equal(method.is("checked"), false);
}());
(function bookend() {
// Do subscribed/unsubscribed cases here.
var args = {

View File

@ -174,6 +174,20 @@ exports.reset_realm_default_language = function () {
$("#id_realm_default_language").val(page_params.realm_default_language);
};
exports.populate_auth_methods = function (auth_methods) {
var auth_methods_table = $("#admin_auth_methods_table").expectOne();
auth_methods_table.find('tr.method_row').remove();
_.each(_.keys(auth_methods).sort(), function (key) {
auth_methods_table.append(templates.render('admin_auth_methods_list', {
method: {
method: key,
enabled: auth_methods[key]
}
}));
});
loading.destroy_indicator($('#admin_page_auth_methods_loading_indicator'));
};
function _setup_page() {
var domains_string = stringify_list_with_conjunction(page_params.domains, "or");
var atdomains = page_params.domains.slice();
@ -189,6 +203,7 @@ function _setup_page() {
realm_restricted_to_domain: page_params.realm_restricted_to_domain,
realm_invite_required: page_params.realm_invite_required,
realm_invite_by_admins_only: page_params.realm_invite_by_admins_only,
realm_authentication_methods: page_params.realm_authentication_methods,
realm_create_stream_by_admins_only: page_params.realm_create_stream_by_admins_only,
realm_allow_message_editing: page_params.realm_allow_message_editing,
realm_message_content_edit_limit_minutes: Math.ceil(page_params.realm_message_content_edit_limit_seconds / 60),
@ -202,6 +217,7 @@ function _setup_page() {
$("#admin-realm-restricted-to-domain-status").expectOne().hide();
$("#admin-realm-invite-required-status").expectOne().hide();
$("#admin-realm-invite-by-admins-only-status").expectOne().hide();
$("#admin-realm-authentication-methods-status").expectOne().hide();
$("#admin-realm-create-stream-by-admins-only-status").expectOne().hide();
$("#admin-realm-message-editing-status").expectOne().hide();
$("#admin-realm-default-language-status").expectOne().hide();
@ -215,6 +231,7 @@ function _setup_page() {
loading.make_indicator($('#admin_page_streams_loading_indicator'));
loading.make_indicator($('#admin_page_deactivated_users_loading_indicator'));
loading.make_indicator($('#admin_page_emoji_loading_indicator'));
loading.make_indicator($('#admin_page_auth_methods_loading_indicator'));
// Populate users and bots tables
channel.get({
@ -234,6 +251,9 @@ function _setup_page() {
error: failed_listing_streams
});
// Populate authentication methods table
exports.populate_auth_methods(page_params.realm_authentication_methods);
// Populate emoji table
exports.populate_emoji(page_params.realm_emoji);
exports.update_default_streams_table();
@ -406,14 +426,16 @@ function _setup_page() {
var restricted_to_domain_status = $("#admin-realm-restricted-to-domain-status").expectOne();
var invite_required_status = $("#admin-realm-invite-required-status").expectOne();
var invite_by_admins_only_status = $("#admin-realm-invite-by-admins-only-status").expectOne();
var authentication_methods_status = $("#admin-realm-authentication-methods-status").expectOne();
var create_stream_by_admins_only_status = $("#admin-realm-create-stream-by-admins-only-status").expectOne();
var message_editing_status = $("#admin-realm-message-editing-status").expectOne();
var default_language_status = $("#admin-realm-default-language-status").expectOne();
var auth_methods_table = $('.admin_auth_methods_table');
name_status.hide();
restricted_to_domain_status.hide();
invite_required_status.hide();
invite_by_admins_only_status.hide();
authentication_methods_status.hide();
create_stream_by_admins_only_status.hide();
message_editing_status.hide();
default_language_status.hide();
@ -429,7 +451,10 @@ function _setup_page() {
var new_allow_message_editing = $("#id_realm_allow_message_editing").prop("checked");
var new_message_content_edit_limit_minutes = $("#id_realm_message_content_edit_limit_minutes").val();
var new_default_language = $("#id_realm_default_language").val();
var new_auth_methods = {};
_.each($("#admin_auth_methods_table").find('tr.method_row'), function (method_row) {
new_auth_methods[$(method_row).data('method')] = $(method_row).find('input').prop('checked');
});
// If allow_message_editing is unchecked, message_content_edit_limit_minutes
// is irrelevant. Hence if allow_message_editing is unchecked, and
// message_content_edit_limit_minutes is poorly formed, we set the latter to
@ -448,6 +473,7 @@ function _setup_page() {
restricted_to_domain: JSON.stringify(new_restricted),
invite_required: JSON.stringify(new_invite),
invite_by_admins_only: JSON.stringify(new_invite_by_admins_only),
authentication_methods: JSON.stringify(new_auth_methods),
create_stream_by_admins_only: JSON.stringify(new_create_stream_by_admins_only),
allow_message_editing: JSON.stringify(new_allow_message_editing),
message_content_edit_limit_seconds: JSON.stringify(parseInt(new_message_content_edit_limit_minutes, 10) * 60),
@ -489,6 +515,11 @@ function _setup_page() {
ui.report_success(i18n.t("Any user may now create new streams!"), create_stream_by_admins_only_status);
}
}
if (response_data.authentication_methods !== undefined) {
if (response_data.authentication_methods) {
ui.report_success(i18n.t("Authentication methods saved!"), authentication_methods_status);
}
}
if (response_data.allow_message_editing !== undefined) {
// We expect message_content_edit_limit_seconds was sent in the
// response as well
@ -515,7 +546,12 @@ function _setup_page() {
}
},
error: function (xhr, error) {
ui.report_error(i18n.t("Failed!"), xhr, name_status);
var reason = $.parseJSON(xhr.responseText).reason;
if (reason === "no authentication") {
ui.report_error(i18n.t("Failed!"), xhr, authentication_methods_status);
} else {
ui.report_error(i18n.t("Failed!"), xhr, name_status);
}
}
});
});

View File

@ -64,6 +64,9 @@ function dispatch_normal_event(event) {
$.each(event.data, function (key, value) {
page_params['realm_' + key] = value;
});
if (event.data.authentication_methods !== undefined) {
admin.populate_auth_methods(event.data.authentication_methods);
}
} else if (event.op === 'update' && event.property === 'default_language') {
page_params.realm_default_language = event.value;
admin.reset_realm_default_language();

View File

@ -49,7 +49,8 @@
"Narrow to private messages with __message.sender_full_name__": "Narrow to private messages with __message.sender_full_name__",
"Pin stream to top<br /> of left sidebar": "Pin stream to top<br /> of left sidebar",
"Password": "Password",
"Any user may now invite new users!": "Any user may now invite new users!",
"Any user may now invite new users!": "Any user may now invite new users!",
"Authentication methods saved!":"Authentication methods saved!",
"Receive desktop <br />notifications": "Receive desktop <br />notifications",
"Please specify a stream": "Please specify a stream",
"Delete Alert Word": "Delete Alert Word",

View File

@ -27,6 +27,7 @@
<div role="tabpanel" class="tab-pane active" id="organization">
{{ partial "organization-settings-admin" }}
{{ partial "emoji-settings-admin" }}
{{ partial "auth-methods-settings-admin" }}
</div>
<div role="tabpanel" class="tab-pane" id="users">
{{ partial "user-list-admin" }}

View File

@ -0,0 +1,22 @@
<div id="organization-settings" class="settings-section">
<div class="settings-section-title"><i class="icon-vector-lock settings-section-icon"></i>
{{t "Authentication Methods" }}</div>
<form class="form-horizontal admin-realm-form">
<div class="alert" id="admin-realm-authentication-methods-status"></div>
<div class="admin-table-wrapper">
<p>{{#tr this}}Configure the authentication methods for the __domain__ organization.{{/tr}}</p>
<table class="table table-condensed table-striped">
<tbody id="admin_auth_methods_table" class=" admin_auth_methods_table">
<th>{{t "Method" }}</th>
<th>{{t "Enabled" }}</th>
</tbody>
</table>
</div>
<div id="admin_page_auth_methods_loading_indicator"></div>
<div class="input-group">
<div class="organization-submission">
<input type="submit" class="button" value="{{t 'Save changes' }}" />
</div>
</div>
</form>
</div>

View File

@ -0,0 +1,11 @@
{{#with method}}
<tr class="method_row" data-method="{{method}}">
<td>
<span class="method">{{method}}</span>
</td>
<td>
<input type="checkbox"
{{#if enabled}}checked="checked"{{/if}} />
</td>
</tr>
{{/with}}

View File

@ -437,6 +437,20 @@ def do_set_realm_invite_by_admins_only(realm, invite_by_admins_only):
)
send_event(event, active_user_ids(realm))
def do_set_realm_authentication_methods(realm, authentication_methods):
# type: (Realm, Dict[str, bool]) -> None
for key, value in list(authentication_methods.items()):
index = getattr(realm.authentication_methods, key).number
realm.authentication_methods.set_bit(index, int(value))
realm.save(update_fields=['authentication_methods'])
event = dict(
type="realm",
op="update_dict",
property='default',
data=dict(authentication_methods=realm.authentication_methods_dict())
)
send_event(event, active_user_ids(realm))
def do_set_realm_create_stream_by_admins_only(realm, create_stream_by_admins_only):
# type: (Realm, bool) -> None
realm.create_stream_by_admins_only = create_stream_by_admins_only
@ -2842,6 +2856,7 @@ def fetch_initial_state_data(user_profile, event_types, queue_id):
state['realm_restricted_to_domain'] = user_profile.realm.restricted_to_domain
state['realm_invite_required'] = user_profile.realm.invite_required
state['realm_invite_by_admins_only'] = user_profile.realm.invite_by_admins_only
state['realm_authentication_methods'] = user_profile.realm.authentication_methods_dict()
state['realm_create_stream_by_admins_only'] = user_profile.realm.create_stream_by_admins_only
state['realm_allow_message_editing'] = user_profile.realm.allow_message_editing
state['realm_message_content_edit_limit_seconds'] = user_profile.realm.message_content_edit_limit_seconds

View File

@ -29,7 +29,7 @@ from confirmation.models import Confirmation
from zproject.backends import ZulipDummyBackend, EmailAuthBackend, \
GoogleMobileOauth2Backend, ZulipRemoteUserBackend, ZulipLDAPAuthBackend, \
ZulipLDAPUserPopulator, DevAuthBackend, GitHubAuthBackend, ZulipAuthMixin, \
password_auth_enabled, github_auth_enabled, AUTH_BACKEND_NAME_MAP
dev_auth_enabled, password_auth_enabled, github_auth_enabled, AUTH_BACKEND_NAME_MAP
from zerver.views.auth import maybe_send_to_registration
@ -1444,3 +1444,41 @@ class TestMaybeSendToRegistration(ZulipTestCase):
confirmation_key = confirmation.confirmation_key
self.assertIn('do_confirm/' + confirmation_key, result.url)
self.assertEqual(PreregistrationUser.objects.all().count(), 1)
class TestAdminSetBackends(ZulipTestCase):
def test_change_enabled_backends(self):
# type: () -> None
# Log in as admin
self.login("iago@zulip.com")
result = self.client_patch("/json/realm", {
'authentication_methods': ujson.dumps({u'Email': False, u'Dev': True})})
self.assert_json_success(result)
realm = get_realm('zulip.com')
self.assertFalse(password_auth_enabled(realm))
self.assertTrue(dev_auth_enabled())
def test_disable_all_backends(self):
# type: () -> None
# Log in as admin
self.login("iago@zulip.com")
result = self.client_patch("/json/realm", {
'authentication_methods' : ujson.dumps({u'Email': False, u'Dev': False})})
self.assert_json_error(result, 'At least one authentication method must be enabled.', status_code=403)
realm = get_realm('zulip.com')
self.assertTrue(password_auth_enabled(realm))
self.assertTrue(dev_auth_enabled())
def test_supported_backends_only_updated(self):
# type: () -> None
# Log in as admin
self.login("iago@zulip.com")
# Set some supported and unsupported backends
result = self.client_patch("/json/realm", {
'authentication_methods' : ujson.dumps({u'Email': False, u'Dev': True, u'GitHub': False})})
self.assert_json_success(result)
realm = get_realm('zulip.com')
# Check that unsupported backend is not enabled
self.assertFalse(github_auth_enabled(realm))
self.assertTrue(dev_auth_enabled())
self.assertFalse(password_auth_enabled(realm))

View File

@ -41,6 +41,7 @@ from zerver.lib.actions import (
do_set_realm_invite_by_admins_only,
do_set_realm_message_editing,
do_set_realm_default_language,
do_set_realm_authentication_methods,
do_update_message,
do_update_pointer,
do_change_twenty_four_hour_time,
@ -466,6 +467,28 @@ class EventsRegisterTest(ZulipTestCase):
error = schema_checker('events[0]', events[0])
self.assert_on_error(error)
def test_change_realm_authentication_methods(self):
# type: () -> None
schema_checker = check_dict([
('type', equals('realm')),
('op', equals('update_dict')),
('property', equals('default')),
('data', check_dict([])),
])
# Test transitions; any new backends should be tested with T/T/T/F/T
for (auth_method_dict) in \
({'Google': True, 'Email': True, 'GitHub': True, 'LDAP': False, 'Dev': False},
{'Google': True, 'Email': True, 'GitHub': False, 'LDAP': False, 'Dev': False},
{'Google': True, 'Email': False, 'GitHub': False, 'LDAP': False, 'Dev': False},
{'Google': True, 'Email': False, 'GitHub': True, 'LDAP': False, 'Dev': False },
{'Google': False, 'Email': False, 'GitHub': False, 'LDAP': False, 'Dev': True},
{'Google': False, 'Email': False, 'GitHub': True, 'LDAP': False, 'Dev': True},
{'Google': False, 'Email': True, 'GitHub': True, 'LDAP': True, 'Dev': False}):
events = self.do_test(lambda: do_set_realm_authentication_methods(self.user_profile.realm,
auth_method_dict))
error = schema_checker('events[0]', events[0])
self.assert_on_error(error)
def test_change_realm_invite_by_admins_only(self):
# type: () -> None
schema_checker = check_dict([

View File

@ -1825,6 +1825,7 @@ class HomeTest(ZulipTestCase):
"product_name",
"prompt_for_invites",
"realm_allow_message_editing",
"realm_authentication_methods",
"realm_create_stream_by_admins_only",
"realm_default_language",
"realm_default_streams",

View File

@ -581,6 +581,7 @@ def home(request):
realm_name = register_ret['realm_name'],
realm_invite_required = register_ret['realm_invite_required'],
realm_invite_by_admins_only = register_ret['realm_invite_by_admins_only'],
realm_authentication_methods = register_ret['realm_authentication_methods'],
realm_create_stream_by_admins_only = register_ret['realm_create_stream_by_admins_only'],
realm_allow_message_editing = register_ret['realm_allow_message_editing'],
realm_message_content_edit_limit_seconds = register_ret['realm_message_content_edit_limit_seconds'],

View File

@ -1,7 +1,6 @@
from __future__ import absolute_import
from typing import Any, Optional
from django.http import HttpRequest, HttpResponse
from django.utils.translation import ugettext as _
@ -14,11 +13,12 @@ from zerver.lib.actions import (
do_set_realm_message_editing,
do_set_realm_restricted_to_domain,
do_set_realm_default_language,
do_set_realm_authentication_methods
)
from zerver.lib.i18n import get_available_language_codes
from zerver.lib.request import has_request_variables, REQ, JsonableError
from zerver.lib.response import json_success, json_error
from zerver.lib.validator import check_string, check_list, check_bool
from zerver.lib.validator import check_string, check_dict, check_bool
from zerver.models import UserProfile
@require_realm_admin
@ -30,12 +30,12 @@ def update_realm(request, user_profile, name=REQ(validator=check_string, default
create_stream_by_admins_only=REQ(validator=check_bool, default=None),
allow_message_editing=REQ(validator=check_bool, default=None),
message_content_edit_limit_seconds=REQ(converter=to_non_negative_int, default=None),
default_language=REQ(validator=check_string, default=None)):
# type: (HttpRequest, UserProfile, Optional[str], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[int], Optional[str]) -> HttpResponse
default_language=REQ(validator=check_string, default=None),
authentication_methods=REQ(validator=check_dict([]), default=None)):
# type: (HttpRequest, UserProfile, Optional[str], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[int], Optional[str], Optional[dict]) -> HttpResponse
# Validation for default_language
if default_language is not None and default_language not in get_available_language_codes():
raise JsonableError(_("Invalid language '%s'" % (default_language,)))
realm = user_profile.realm
data = {} # type: Dict[str, Any]
if name is not None and realm.name != name:
@ -50,6 +50,13 @@ def update_realm(request, user_profile, name=REQ(validator=check_string, default
if invite_by_admins_only is not None and realm.invite_by_admins_only != invite_by_admins_only:
do_set_realm_invite_by_admins_only(realm, invite_by_admins_only)
data['invite_by_admins_only'] = invite_by_admins_only
if authentication_methods is not None and realm.authentication_methods != authentication_methods:
if True not in list(authentication_methods.values()):
return json_error(_("At least one authentication method must be enabled."),
data={"reason": "no authentication"}, status=403)
else:
do_set_realm_authentication_methods(realm, authentication_methods)
data['authentication_methods'] = authentication_methods
if create_stream_by_admins_only is not None and realm.create_stream_by_admins_only != create_stream_by_admins_only:
do_set_realm_create_stream_by_admins_only(realm, create_stream_by_admins_only)
data['create_stream_by_admins_only'] = create_stream_by_admins_only