Manage allowed domains from admin settings.

Fixes: #1867.
This commit is contained in:
Harshit Bansal
2016-12-26 23:49:02 +05:30
committed by Tim Abbott
parent a39643da61
commit ceb636dbd9
15 changed files with 326 additions and 37 deletions

View File

@@ -91,6 +91,32 @@ function render(template_name, args) {
global.write_handlebars_output("actions_popover_content", html); global.write_handlebars_output("actions_popover_content", html);
}()); }());
(function admin_alias_list() {
var html = "<table>";
var args = {
alias: {
id: 1,
domain: 'zulip.org',
},
};
html += render("admin_alias_list", args);
html += "</table>";
var button = $(html).find('.btn');
var domain = $(html).find('.domain');
var row = button.closest('tr');
assert.equal(button.text().trim(), "Delete");
assert(button.hasClass("delete_alias"));
assert.equal(button.data("id"), "1");
assert.equal(domain.text(), "zulip.org");
assert.equal(row.attr("id"), "alias_1");
global.write_handlebars_output("admin_alias_list", html);
}());
(function admin_default_streams_list() { (function admin_default_streams_list() {
var html = '<table>'; var html = '<table>';
var streams = ['devel', 'trac', 'zulip']; var streams = ['devel', 'trac', 'zulip'];

View File

@@ -200,6 +200,21 @@ exports.populate_filters = function (filters_data) {
loading.destroy_indicator($('#admin_page_filters_loading_indicator')); loading.destroy_indicator($('#admin_page_filters_loading_indicator'));
}; };
exports.populate_realm_aliases = function (aliases) {
var alias_table = $("#alias_table").expectOne();
var domains_list = _.map(page_params.domains, function (ADomain) {
return ADomain.domain;
});
var domains = stringify_list_with_conjunction(domains_list, "or");
$("#realm_restricted_to_domains_label").text(i18n.t("Users restricted to __domains__", {domains: domains}));
alias_table.find("tr").remove();
_.each(aliases, function (alias) {
alias_table.append(templates.render("admin_alias_list", {alias: alias}));
});
};
exports.reset_realm_default_language = function () { exports.reset_realm_default_language = function () {
$("#id_realm_default_language").val(page_params.realm_default_language); $("#id_realm_default_language").val(page_params.realm_default_language);
}; };
@@ -219,17 +234,8 @@ exports.populate_auth_methods = function (auth_methods) {
}; };
function _setup_page() { function _setup_page() {
var domains_string = stringify_list_with_conjunction(page_params.domains, "or");
var atdomains = page_params.domains.slice();
var i;
for (i = 0; i < atdomains.length; i += 1) {
atdomains[i] = '@' + atdomains[i];
}
var atdomains_string = stringify_list_with_conjunction(atdomains, "or");
var options = { var options = {
realm_name: page_params.realm_name, realm_name: page_params.realm_name,
domains_string: domains_string,
atdomains_string: atdomains_string,
realm_restricted_to_domain: page_params.realm_restricted_to_domain, realm_restricted_to_domain: page_params.realm_restricted_to_domain,
realm_invite_required: page_params.realm_invite_required, realm_invite_required: page_params.realm_invite_required,
realm_invite_by_admins_only: page_params.realm_invite_by_admins_only, realm_invite_by_admins_only: page_params.realm_invite_by_admins_only,
@@ -300,6 +306,9 @@ function _setup_page() {
// Populate filters table // Populate filters table
exports.populate_filters(page_params.realm_filters); exports.populate_filters(page_params.realm_filters);
// Populate realm aliases
exports.populate_realm_aliases(page_params.domains);
// Setup click handlers // Setup click handlers
$(".admin_user_table").on("click", ".deactivate", function (e) { $(".admin_user_table").on("click", ".deactivate", function (e) {
e.preventDefault(); e.preventDefault();
@@ -569,6 +578,15 @@ function _setup_page() {
} }
if (response_data.restricted_to_domain !== undefined) { if (response_data.restricted_to_domain !== undefined) {
if (response_data.restricted_to_domain) { if (response_data.restricted_to_domain) {
var atdomains = _.map(page_params.domains, function (ADomain) {
return ADomain.domain;
});
var i;
for (i = 0; i < atdomains.length; i += 1) {
atdomains[i] = '@' + atdomains[i];
}
var atdomains_string = stringify_list_with_conjunction(atdomains, "or");
ui.report_success(i18n.t("New users must have e-mails ending in __atdomains_string__!", {atdomains_string: atdomains_string}), restricted_to_domain_status); ui.report_success(i18n.t("New users must have e-mails ending in __atdomains_string__!", {atdomains_string: atdomains_string}), restricted_to_domain_status);
} else { } else {
ui.report_success(i18n.t("New users may have arbitrary e-mails!"), restricted_to_domain_status); ui.report_success(i18n.t("New users may have arbitrary e-mails!"), restricted_to_domain_status);
@@ -889,6 +907,48 @@ function _setup_page() {
}); });
}); });
$("#alias_table").on("click", ".delete_alias", function () {
var url = "/json/realm/domains/" + $(this).data('id');
var aliases_info = $("#realm_aliases_modal").find(".aliases_info");
channel.del({
url: url,
success: function () {
aliases_info.removeClass("text-error");
aliases_info.addClass("text-success");
aliases_info.text("Deleted successfully!");
},
error: function (xhr) {
aliases_info.removeClass("text-success");
aliases_info.addClass("text-error");
aliases_info.text(JSON.parse(xhr.responseText).msg);
}
});
});
$("#add_alias").click(function () {
var aliases_info = $("#realm_aliases_modal").find(".aliases_info");
var data = {
domain: $("#new_alias").val(),
};
channel.post({
url: "/json/realm/domains",
data: data,
success: function () {
$("#new_alias").val("");
aliases_info.removeClass("text-error");
aliases_info.addClass("text-success");
aliases_info.text("Added successfully!");
},
error: function (xhr) {
aliases_info.removeClass("text-success");
aliases_info.addClass("text-error");
aliases_info.text(JSON.parse(xhr.responseText).msg);
}
});
});
} }
exports.setup_page = function () { exports.setup_page = function () {

View File

@@ -107,6 +107,20 @@ function dispatch_normal_event(event) {
admin.populate_filters(page_params.realm_filters); admin.populate_filters(page_params.realm_filters);
break; break;
case 'realm_domains':
if (event.op === 'add') {
page_params.domains.push(event.alias);
} else if (event.op === 'remove') {
var i;
for (i = 0;i < page_params.domains.length;i += 1) {
if (page_params.domains[i].id === event.alias_id) {
page_params.domains.splice(i, 1);
break;
}
}
}
admin.populate_realm_aliases(page_params.domains);
break;
case 'realm_user': case 'realm_user':
if (event.op === 'add') { if (event.op === 'add') {
people.add_in_realm(event.person); people.add_in_realm(event.person);

View File

@@ -430,3 +430,13 @@ input[type=checkbox].inline-block {
#administration #deactivation_stream_modal h3 { #administration #deactivation_stream_modal h3 {
overflow-wrap: break-word; overflow-wrap: break-word;
} }
#administration .centered-footer {
text-align: center;
padding: 10px 15px;
}
#administration #new_alias {
margin-right: 20px;
margin-bottom: auto;
}

View File

@@ -74,6 +74,23 @@
<button class="button btn-danger" id="do_deactivate_stream_button">{{t "Yes, delete this stream" }}</button> <button class="button btn-danger" id="do_deactivate_stream_button">{{t "Yes, delete this stream" }}</button>
</div> </div>
</div> </div>
<div id="realm_aliases_modal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="realm_aliases_modal_label" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="realm_aliases_modal_label">{{t "Allowed Domains" }}</h3>
</div>
<div class="modal-body">
<table class="table table-condensed table-stripped" id="alias_table">
<th>{{t "Domain" }}</th>
<th>{{t "Action" }}</th>
</table>
</div>
<div class="modal-footer centered-footer">
<input type="text" id="new_alias"></input>
<button type="button" id="add_alias">{{t "Add" }}</button>
<div class="aliases_info"></div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,6 @@
{{#with alias}}
<tr id="alias_{{id}}">
<td class="domain">{{domain}}</td>
<td><button class="btn btn-danger btn-sm delete_alias" data-id="{{id}}">{{t "Delete" }}</button></td>
</tr>
{{/with}}

View File

@@ -20,10 +20,10 @@
<div class="input-group"> <div class="input-group">
<input type="checkbox" class="inline-block" id="id_realm_restricted_to_domain" name="realm_restricted_to_domain" <input type="checkbox" class="inline-block" id="id_realm_restricted_to_domain" name="realm_restricted_to_domain"
{{#if realm_restricted_to_domain}}checked="checked"{{/if}} /> {{#if realm_restricted_to_domain}}checked="checked"{{/if}} />
<label for="id_realm_restricted_to_domain" class="inline-block" <label for="id_realm_restricted_to_domain" class="inline-block" id="realm_restricted_to_domains_label"
title="{{#tr this}}If checked, only users with an e-mail address ending in __atdomains_string__ will be able to join the organization.{{/tr}}"> title="{{#tr this}}If checked, only users with an e-mail address ending in these domains will be able to join the organization.{{/tr}}">
{{#tr this}}Users restricted to __domains_string__{{/tr}}
</label> </label>
<a data-toggle="modal" href="#realm_aliases_modal">{{t "Change domains" }}</a>
</div> </div>
<div class="input-group"> <div class="input-group">
<input type="checkbox" class="inline-block" id="id_realm_invite_required" name="realm_invite_required" <input type="checkbox" class="inline-block" id="id_realm_invite_required" name="realm_invite_required"

View File

@@ -3091,6 +3091,9 @@ def fetch_initial_state_data(user_profile, event_types, queue_id):
if want('realm_domain'): if want('realm_domain'):
state['realm_domain'] = user_profile.realm.domain state['realm_domain'] = user_profile.realm.domain
if want('realm_domains'):
state['realm_domains'] = do_get_realm_aliases(user_profile.realm)
if want('realm_emoji'): if want('realm_emoji'):
state['realm_emoji'] = user_profile.realm.get_emoji() state['realm_emoji'] = user_profile.realm.get_emoji()
@@ -3309,6 +3312,11 @@ def apply_events(state, events, user_profile):
elif event['type'] == "update_message_flags": elif event['type'] == "update_message_flags":
# The client will get the message with the updated flags directly # The client will get the message with the updated flags directly
pass pass
elif event['type'] == "realm_domains":
if event['op'] == 'add':
state['realm_domains'].append(event['alias'])
elif event['op'] == 'remove':
state['realm_domains'] = [alias for alias in state['realm_domains'] if alias['id'] != event['alias_id']]
elif event['type'] == "realm_emoji": elif event['type'] == "realm_emoji":
state['realm_emoji'] = event['realm_emoji'] state['realm_emoji'] = event['realm_emoji']
elif event['type'] == "alert_words": elif event['type'] == "alert_words":
@@ -3661,9 +3669,27 @@ def get_emails_from_user_ids(user_ids):
# We may eventually use memcached to speed this up, but the DB is fast. # We may eventually use memcached to speed this up, but the DB is fast.
return UserProfile.emails_from_ids(user_ids) return UserProfile.emails_from_ids(user_ids)
def realm_aliases(realm): def do_get_realm_aliases(realm):
# type: (Realm) -> List[Text] # type: (Realm) -> List[Dict[str, Text]]
return [alias.domain for alias in realm.realmalias_set.all()] return list(realm.realmalias_set.values('id', 'domain'))
def do_add_realm_alias(realm, domain):
# type: (Realm, Text) -> (RealmAlias)
alias = RealmAlias(realm=realm, domain=domain)
alias.full_clean()
alias.save()
event = dict(type="realm_domains", op="add",
alias=dict(id=alias.id,
domain=alias.domain,
))
send_event(event, active_user_ids(realm))
return alias
def do_remove_realm_alias(realm, alias_id):
# type: (Realm, int) -> None
RealmAlias.objects.get(pk=alias_id).delete()
event = dict(type="realm_domains", op="remove", alias_id=alias_id)
send_event(event, active_user_ids(realm))
def get_occupied_streams(realm): def get_occupied_streams(realm):
# type: (Realm) -> QuerySet # type: (Realm) -> QuerySet

View File

@@ -6,7 +6,7 @@ from typing import Any
from argparse import ArgumentParser from argparse import ArgumentParser
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from zerver.models import Realm, RealmAlias, get_realm, can_add_alias from zerver.models import Realm, RealmAlias, get_realm, can_add_alias
from zerver.lib.actions import realm_aliases from zerver.lib.actions import do_get_realm_aliases
import sys import sys
class Command(BaseCommand): class Command(BaseCommand):
@@ -32,19 +32,19 @@ class Command(BaseCommand):
realm = get_realm(options["string_id"]) realm = get_realm(options["string_id"])
if options["op"] == "show": if options["op"] == "show":
print("Aliases for %s:" % (realm.domain,)) print("Aliases for %s:" % (realm.domain,))
for alias in realm_aliases(realm): for alias in do_get_realm_aliases(realm):
print(alias) print(alias["domain"])
sys.exit(0) sys.exit(0)
alias = options['alias'].lower() domain = options['alias'].lower()
if options["op"] == "add": if options["op"] == "add":
if not can_add_alias(alias): if not can_add_alias(domain):
print("A Realm already exists for this domain, cannot add it as an alias for another realm!") print("A Realm already exists for this domain, cannot add it as an alias for another realm!")
sys.exit(1) sys.exit(1)
RealmAlias.objects.create(realm=realm, domain=alias) RealmAlias.objects.create(realm=realm, domain=domain)
sys.exit(0) sys.exit(0)
elif options["op"] == "remove": elif options["op"] == "remove":
RealmAlias.objects.get(realm=realm, domain=alias).delete() RealmAlias.objects.get(realm=realm, domain=domain).delete()
sys.exit(0) sys.exit(0)
else: else:
self.print_help("./manage.py", "realm_alias") self.print_help("./manage.py", "realm_alias")

View File

@@ -339,7 +339,7 @@ def email_allowed_for_realm(email, realm):
def list_of_domains_for_realm(realm): def list_of_domains_for_realm(realm):
# type: (Realm) -> List[Text] # type: (Realm) -> List[Text]
return list(RealmAlias.objects.filter(realm = realm).values_list('domain', flat=True)) return list(RealmAlias.objects.filter(realm = realm).values('domain', 'id'))
class RealmEmoji(ModelReprMixin, models.Model): class RealmEmoji(ModelReprMixin, models.Model):
author = models.ForeignKey('UserProfile', blank=True, null=True) author = models.ForeignKey('UserProfile', blank=True, null=True)

View File

@@ -8,7 +8,7 @@ from django.test import TestCase
from zerver.models import ( from zerver.models import (
get_client, get_realm, get_stream, get_user_profile_by_email, get_client, get_realm, get_stream, get_user_profile_by_email,
Message, Recipient, UserProfile Message, RealmAlias, Recipient, UserProfile
) )
from zerver.lib.actions import ( from zerver.lib.actions import (
@@ -56,6 +56,8 @@ from zerver.lib.actions import (
do_change_enable_online_push_notifications, do_change_enable_online_push_notifications,
do_change_pm_content_in_desktop_notifications, do_change_pm_content_in_desktop_notifications,
do_change_enable_digest_emails, do_change_enable_digest_emails,
do_add_realm_alias,
do_remove_realm_alias,
fetch_initial_state_data, fetch_initial_state_data,
get_subscription get_subscription
) )
@@ -807,6 +809,31 @@ class EventsRegisterTest(ZulipTestCase):
error = schema_checker('events[0]', events[0]) error = schema_checker('events[0]', events[0])
self.assert_on_error(error) self.assert_on_error(error)
def test_realm_alias_events(self):
# type: () -> None
schema_checker = check_dict([
('type', equals('realm_domains')),
('op', equals('add')),
('alias', check_dict([
('id', check_int),
('domain', check_string),
])),
])
realm = get_realm('zulip')
events = self.do_test(lambda: do_add_realm_alias(realm, 'zulip.org'))
error = schema_checker('events[0]', events[0])
self.assert_on_error(error)
schema_checker = check_dict([
('type', equals('realm_domains')),
('op', equals('remove')),
('alias_id', check_int),
])
alias_id = RealmAlias.objects.get(realm=realm, domain='zulip.org').id
events = self.do_test(lambda: do_remove_realm_alias(realm, alias_id))
error = schema_checker('events[0]', events[0])
self.assert_on_error(error)
def test_create_bot(self): def test_create_bot(self):
# type: () -> None # type: () -> None
bot_created_checker = check_dict([ bot_created_checker = check_dict([

View File

@@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from zerver.lib.test_classes import ZulipTestCase
from zerver.models import get_realm, get_realm_by_email_domain, \
GetRealmByDomainException, RealmAlias
import ujson
class RealmAliasTest(ZulipTestCase):
def test_list(self):
# type: () -> None
self.login("iago@zulip.com")
realm = get_realm('zulip')
alias = RealmAlias(realm=realm, domain='zulip.org')
alias.save()
result = self.client_get("/json/realm/domains")
self.assert_json_success(result)
self.assertEqual(200, result.status_code)
content = ujson.loads(result.content)
self.assertEqual(len(content['domains']), 2)
def test_not_realm_admin(self):
# type: () -> None
self.login("hamlet@zulip.com")
result = self.client_post("/json/realm/domains")
self.assert_json_error(result, 'Must be a realm administrator')
result = self.client_delete("/json/realm/domains/15")
self.assert_json_error(result, 'Must be a realm administrator')
def test_create(self):
# type: () -> None
self.login("iago@zulip.com")
data = {"domain": ""}
result = self.client_post("/json/realm/domains", info=data)
self.assert_json_error(result, 'Domain can\'t be empty.')
data['domain'] = 'zulip.org'
result = self.client_post("/json/realm/domains", info=data)
self.assert_json_success(result)
result = self.client_post("/json/realm/domains", info=data)
self.assert_json_error(result, 'A Realm for this domain already exists.')
def test_delete(self):
# type: () -> None
self.login("iago@zulip.com")
realm = get_realm('zulip')
alias_id = RealmAlias.objects.create(realm=realm, domain='zulip.org').id
aliases_count = RealmAlias.objects.count()
result = self.client_delete("/json/realm/domains/{0}".format(alias_id + 1))
self.assert_json_error(result, 'No such entry found.')
result = self.client_delete("/json/realm/domains/{0}".format(alias_id))
self.assert_json_success(result)
self.assertEqual(RealmAlias.objects.count(), aliases_count - 1)
def test_get_realm_by_email_domain(self):
# type: () -> None
self.assertEqual(get_realm_by_email_domain('user@zulip.com').string_id, 'zulip')
self.assertEqual(get_realm_by_email_domain('user@fakedomain.com'), None)
with self.settings(REALMS_HAVE_SUBDOMAINS = True), (
self.assertRaises(GetRealmByDomainException)):
get_realm_by_email_domain('user@zulip.com')

View File

@@ -22,9 +22,8 @@ from zerver.forms import WRONG_SUBDOMAIN_ERROR
from zerver.models import UserProfile, Recipient, \ from zerver.models import UserProfile, Recipient, \
Realm, RealmAlias, UserActivity, \ Realm, RealmAlias, UserActivity, \
get_user_profile_by_email, get_realm, get_realm_by_email_domain, \ get_user_profile_by_email, get_realm, get_client, get_stream, \
get_client, get_stream, Message, get_unique_open_realm, \ Message, get_unique_open_realm, completely_open
completely_open, GetRealmByDomainException
from zerver.lib.avatar import get_avatar_url from zerver.lib.avatar import get_avatar_url
from zerver.lib.initial_password import initial_password from zerver.lib.initial_password import initial_password
@@ -271,16 +270,6 @@ class RealmTest(ZulipTestCase):
self.assertNotEqual(realm.default_language, invalid_lang) self.assertNotEqual(realm.default_language, invalid_lang)
class RealmAliasTest(ZulipTestCase):
def test_get_realm_by_email_domain(self):
# type: () -> None
self.assertEqual(get_realm_by_email_domain('user@zulip.com').string_id, 'zulip')
self.assertEqual(get_realm_by_email_domain('user@fakedomain.com'), None)
with self.settings(REALMS_HAVE_SUBDOMAINS = True), (
self.assertRaises(GetRealmByDomainException)):
get_realm_by_email_domain('user@zulip.com')
class PermissionTest(ZulipTestCase): class PermissionTest(ZulipTestCase):
def test_get_admin_users(self): def test_get_admin_users(self):
# type: () -> None # type: () -> None

View File

@@ -0,0 +1,42 @@
from __future__ import absolute_import
from django.core.exceptions import ValidationError
from django.http import HttpRequest, HttpResponse
from django.utils.translation import ugettext as _
from zerver.decorator import has_request_variables, REQ, require_realm_admin
from zerver.lib.actions import do_add_realm_alias, do_get_realm_aliases, \
do_remove_realm_alias
from zerver.lib.response import json_error, json_success
from zerver.models import can_add_alias, RealmAlias, UserProfile
from typing import Text
def list_aliases(request, user_profile):
# type: (HttpRequest, UserProfile) -> (HttpResponse)
aliases = do_get_realm_aliases(user_profile.realm)
return json_success({'domains': aliases})
@require_realm_admin
@has_request_variables
def create_alias(request, user_profile, domain=REQ()):
# type: (HttpRequest, UserProfile, Text) -> (HttpResponse)
if can_add_alias(domain):
try:
alias = do_add_realm_alias(user_profile.realm, domain)
except ValidationError:
return json_error(_('Domain can\'t be empty.'))
else:
return json_error(_('A Realm for this domain already exists.'))
return json_success({'new_domain': [alias.id, alias.domain]})
@require_realm_admin
@has_request_variables
def delete_alias(request, user_profile, alias_id):
# type: (HttpRequest, UserProfile, int) -> (HttpResponse)
try:
# Ensure alias_id is an integer. Django passes captured url parameters as strings.
do_remove_realm_alias(user_profile.realm, int(alias_id))
except RealmAlias.DoesNotExist:
return json_error(_('No such entry found.'))
return json_success()

View File

@@ -159,6 +159,13 @@ v1_api_and_json_patterns = [
# Returns a 204, used by desktop app to verify connectivity status # Returns a 204, used by desktop app to verify connectivity status
url(r'generate_204$', zerver.views.generate_204, name='zerver.views.generate_204'), url(r'generate_204$', zerver.views.generate_204, name='zerver.views.generate_204'),
# realm/aliases -> zerver.views.realm_aliases
url(r'^realm/domains$', rest_dispatch,
{'GET': 'zerver.views.realm_aliases.list_aliases',
'POST': 'zerver.views.realm_aliases.create_alias'}),
url(r'^realm/domains/(?P<alias_id>\d+)$', rest_dispatch,
{'DELETE': 'zerver.views.realm_aliases.delete_alias'}),
# realm/emoji -> zerver.views.realm_emoji # realm/emoji -> zerver.views.realm_emoji
url(r'^realm/emoji$', rest_dispatch, url(r'^realm/emoji$', rest_dispatch,
{'GET': 'zerver.views.realm_emoji.list_emoji', {'GET': 'zerver.views.realm_emoji.list_emoji',