From ea39fb25565ffdadb9060406dba60b40b2b89eac Mon Sep 17 00:00:00 2001 From: hackerkid Date: Tue, 19 Jul 2016 18:05:08 +0530 Subject: [PATCH] Add option for hosting each realm on its own subdomain. This adds support for running a Zulip production server with each realm on its own unique subdomain, e.g. https://realm_name.example.com. This patch includes a ton of important features: * Configuring the Zulip sesion middleware to issue cookier correctly for the subdomains case. * Throwing an error if the user tries to visit an invalid subdomain. * Runs a portion of the Casper tests with REALMS_HAVE_SUBDOMAINS enabled to test the subdomain signup process. * Updating our integrations documentation to refer to the current subdomain. * Enforces that users can only login to the subdomain of their realm (but does not restrict the API; that will be tightened in a future commit). Note that toggling settings.REALMS_HAVE_SUBDOMAINS on a live server is not supported without manual intervention (the main problem will be adding "subdomain" values for all the existing realms). [substantially modified by tabbott as part of merging] --- frontend_tests/casper_lib/common.js | 9 ++- .../casper_tests/00-realm-creation.js | 43 ++++++++--- frontend_tests/casper_tests/01-login.js | 9 ++- frontend_tests/casper_tests/06-settings.js | 10 ++- frontend_tests/casper_tests/10-admin.js | 4 +- frontend_tests/run-casper | 16 +++- templates/zerver/invalid_realm.html | 9 +++ templates/zerver/register.html | 20 ++++- zerver/decorator.py | 4 +- zerver/forms.py | 35 ++++++++- zerver/lib/actions.py | 6 +- zerver/lib/test_helpers.py | 12 ++- zerver/lib/utils.py | 19 +++++ zerver/middleware.py | 27 ++++++- zerver/migrations/0029_realm_subdomain.py | 36 +++++++++ zerver/models.py | 13 ++++ zerver/tests/test_integrations.py | 25 ++++++- zerver/tests/test_signup.py | 51 +++++++++++++ zerver/views/__init__.py | 73 +++++++++++++++---- zerver/views/integrations.py | 29 ++++++-- zilencer/management/commands/populate_db.py | 2 +- zproject/backends.py | 62 +++++++++++----- zproject/dev_settings.py | 7 +- zproject/settings.py | 1 + zproject/test_settings.py | 2 + 25 files changed, 442 insertions(+), 82 deletions(-) create mode 100644 templates/zerver/invalid_realm.html create mode 100644 zerver/migrations/0029_realm_subdomain.py diff --git a/frontend_tests/casper_lib/common.js b/frontend_tests/casper_lib/common.js index e9074efdf7..80eb25cacb 100644 --- a/frontend_tests/casper_lib/common.js +++ b/frontend_tests/casper_lib/common.js @@ -1,3 +1,4 @@ +var REALMS_HAVE_SUBDOMAINS = casper.cli.get('subdomains'); var common = (function () { var exports = {}; @@ -83,7 +84,13 @@ exports.then_log_in = function (credentials) { }; exports.start_and_log_in = function (credentials, viewport) { - casper.start('http://127.0.0.1:9981/accounts/login', function () { + var log_in_url = ""; + if (REALMS_HAVE_SUBDOMAINS) { + log_in_url = "http://zulip.zulipdev.com:9981/accounts/login"; + } else { + log_in_url = "http://localhost:9981/accounts/login"; + } + casper.start(log_in_url, function () { exports.initialize_casper(viewport); log_in(credentials); }); diff --git a/frontend_tests/casper_tests/00-realm-creation.js b/frontend_tests/casper_tests/00-realm-creation.js index b64ba1a7ba..2c23bcba20 100644 --- a/frontend_tests/casper_tests/00-realm-creation.js +++ b/frontend_tests/casper_tests/00-realm-creation.js @@ -2,9 +2,20 @@ var common = require('../casper_lib/common.js').common; var email = 'alice@test.example.com'; var domain = 'test.example.com'; +var subdomain = 'testsubdomain'; var organization_name = 'Awesome Organization'; +var REALMS_HAVE_SUBDOMAINS = casper.cli.get('subdomains'); +var host; +var realm_host; -casper.start('http://127.0.0.1:9981/create_realm/'); +if (REALMS_HAVE_SUBDOMAINS) { + host = 'zulipdev.com:9981'; + realm_host = subdomain + '.' + host; +} else { + host = realm_host = 'localhost:9981'; +} + +casper.start('http://' + host + '/create_realm/'); casper.then(function () { // Submit the email for realm creation @@ -21,12 +32,12 @@ casper.then(function () { }); // Special endpoint enabled only during tests for extracting confirmation key -casper.thenOpen('http://127.0.0.1:9981/confirmation_key/'); +casper.thenOpen('http://' + host + '/confirmation_key/'); // Open the confirmation URL casper.then(function () { var confirmation_key = JSON.parse(this.getPageContent()).confirmation_key; - var confirmation_url = 'http://127.0.0.1:9981/accounts/do_confirm/' + confirmation_key; + var confirmation_url = 'http://' + host + '/accounts/do_confirm/' + confirmation_key; this.thenOpen(confirmation_url); }); @@ -47,16 +58,26 @@ casper.then(function () { casper.then(function () { this.waitForSelector('form[action^="/accounts/register/"]', function () { - this.fill('form[action^="/accounts/register/"]', { - full_name: 'Alice', - realm_name: organization_name, - password: 'password', - terms: true - }, true); + if (REALMS_HAVE_SUBDOMAINS) { + this.fill('form[action^="/accounts/register/"]', { + full_name: 'Alice', + realm_name: organization_name, + realm_subdomain: subdomain, + password: 'password', + terms: true + }, true); + } else { + this.fill('form[action^="/accounts/register/"]', { + full_name: 'Alice', + realm_name: organization_name, + password: 'password', + terms: true + }, true); + } }); this.waitWhileSelector('form[action^="/accounts/register/"]', function () { - casper.test.assertUrlMatch('http://127.0.0.1:9981/invite/', 'Invite more users page loaded'); + casper.test.assertUrlMatch(realm_host + '/invite/', 'Invite more users page loaded'); }); }); @@ -75,7 +96,7 @@ casper.then(function () { }); this.waitWhileSelector('#submit_invitation', function () { - this.test.assertUrlMatch('http://127.0.0.1:9981/', 'Realm created and logged in'); + this.test.assertUrlMatch(realm_host, 'Realm created and logged in'); }); }); diff --git a/frontend_tests/casper_tests/01-login.js b/frontend_tests/casper_tests/01-login.js index b0a3489469..2ca6d954e4 100644 --- a/frontend_tests/casper_tests/01-login.js +++ b/frontend_tests/casper_tests/01-login.js @@ -1,7 +1,14 @@ var common = require('../casper_lib/common.js').common; +var REALMS_HAVE_SUBDOMAINS = casper.cli.get('subdomains'); +var realm_url = ""; +if (REALMS_HAVE_SUBDOMAINS) { + realm_url = "http://zulip.zulipdev.com:9981/"; +} else { + realm_url = "http://localhost:9981/"; +} // Start of test script. -casper.start('http://127.0.0.1:9981/', common.initialize_casper); +casper.start(realm_url, common.initialize_casper); casper.then(function () { casper.test.assertHttpStatus(302); diff --git a/frontend_tests/casper_tests/06-settings.js b/frontend_tests/casper_tests/06-settings.js index dc3b5bb853..8b7b4f34ef 100644 --- a/frontend_tests/casper_tests/06-settings.js +++ b/frontend_tests/casper_tests/06-settings.js @@ -1,5 +1,6 @@ var common = require('../casper_lib/common.js').common; var test_credentials = require('../../var/casper/test_credentials.js').test_credentials; +var REALMS_HAVE_SUBDOMAINS = casper.cli.get('subdomains'); common.start_and_log_in(); @@ -166,7 +167,14 @@ casper.waitForSelector("#default_language", function () { casper.test.info("Opening German page through i18n url."); }); -casper.thenOpen('http://127.0.0.1:9981/de/#settings'); +var settings_url = ""; +if (REALMS_HAVE_SUBDOMAINS) { + settings_url = 'http://zulip.zulipdev.com:9981/de/#settings'; +} else { + settings_url = 'http://localhost:9981/de/#settings'; +} + +casper.thenOpen(settings_url); casper.waitForSelector("#settings-change-box", function check_url_preference() { casper.test.info("Checking the i18n url language precedence."); diff --git a/frontend_tests/casper_tests/10-admin.js b/frontend_tests/casper_tests/10-admin.js index f2c96842c2..79c65bea90 100644 --- a/frontend_tests/casper_tests/10-admin.js +++ b/frontend_tests/casper_tests/10-admin.js @@ -144,7 +144,7 @@ casper.then(function () { casper.waitForSelector('.admin-emoji-form', function () { casper.fill('form.admin-emoji-form', { 'name': 'MouseFace', - 'url': 'http://127.0.0.1:9991/static/images/integrations/logos/jenkins.png' + 'url': 'http://localhost:9991/static/images/integrations/logos/jenkins.png' }); casper.click('form.admin-emoji-form input.button'); }); @@ -159,7 +159,7 @@ casper.then(function () { casper.then(function () { casper.waitForSelector('.emoji_row', function () { casper.test.assertSelectorHasText('.emoji_row .emoji_name', 'MouseFace'); - casper.test.assertExists('.emoji_row img[src="http://127.0.0.1:9991/static/images/integrations/logos/jenkins.png"]'); + casper.test.assertExists('.emoji_row img[src="http://localhost:9991/static/images/integrations/logos/jenkins.png"]'); casper.click('.emoji_row button.delete'); }); }); diff --git a/frontend_tests/run-casper b/frontend_tests/run-casper index 6f158a1682..00f7f6f5f7 100755 --- a/frontend_tests/run-casper +++ b/frontend_tests/run-casper @@ -64,8 +64,8 @@ def server_is_up(server): except: return False -def run_tests(files): - # type: (Iterable[str]) -> None +def run_tests(realms_have_subdomains, files): + # type: (bool, Iterable[str]) -> None test_files = [] for file in files: if not os.path.exists(file): @@ -78,7 +78,7 @@ def run_tests(files): if options.remote_debug: remote_debug = "--remote-debugger-port=7777 --remote-debugger-autorun=yes" - cmd = "frontend_tests/casperjs/bin/casperjs %s test " % (remote_debug,) + cmd = "frontend_tests/casperjs/bin/casperjs %s test --subdomains=%s " % (remote_debug, realms_have_subdomains,) if test_files: cmd += ' '.join(test_files) else: @@ -115,5 +115,13 @@ Oops, the frontend tests failed. Tips for debugging: sys.exit(ret) -run_tests(args) +# Run tests with REALMS_HAVE_SUBDOMAINS set to True +run_tests(False, args) +os.environ["EXTERNAL_HOST"] = "zulipdev.com:9981" +os.environ["REALMS_HAVE_SUBDOMAINS"] = "True" +# Run tests with REALMS_HAVE_SUBDOMAINS set to True +if len(args) == 0: + run_tests(True, ["00-realm-creation.js", "01-login.js", "02-site.js"]) +else: + run_tests(True, args) sys.exit(0) diff --git a/templates/zerver/invalid_realm.html b/templates/zerver/invalid_realm.html new file mode 100644 index 0000000000..f8ca55b783 --- /dev/null +++ b/templates/zerver/invalid_realm.html @@ -0,0 +1,9 @@ +{% extends "zerver/portico.html" %} +{% block portico_content %} + +

{{ _('Organization Does Not Exist') }}

+ +

{{ _('Hi there! Thank you for your interest in Zulip') }}.

+

{% trans %}There is no Zulip organization hosted at this subdomain{% endtrans %}.

+ +{% endblock %} diff --git a/templates/zerver/register.html b/templates/zerver/register.html index 059a48cbb5..f31dd02fd0 100644 --- a/templates/zerver/register.html +++ b/templates/zerver/register.html @@ -33,6 +33,7 @@ Form is validated both client-side using jquery-validate (see signup.js) and ser +
@@ -51,7 +52,7 @@ Form is validated both client-side using jquery-validate (see signup.js) and ser
- {% if creating_new_team %} + {% if creating_new_team %}
@@ -65,7 +66,24 @@ Form is validated both client-side using jquery-validate (see signup.js) and ser {% endif %}
{{ _('You can change this later on the admin page.') }}
+
+ {% if realms_have_subdomains %} +
+ +
+ .{{ external_host }} + {% if form.realm_subdomain.errors %} + {% for error in form.realm_subdomain.errors %} +
{{ error }}
+ {% endfor %} + {% endif %} +

{% trans %}The address you'll use to sign in to your organization.{% endtrans %}.

+
+ {% endif %} + {% endif %} {% if password_auth_enabled %} diff --git a/zerver/decorator.py b/zerver/decorator.py index e6707cf700..5b9b06205b 100644 --- a/zerver/decorator.py +++ b/zerver/decorator.py @@ -14,7 +14,7 @@ from django.utils.timezone import now from django.conf import settings from zerver.lib.queue import queue_json_publish from zerver.lib.timestamp import datetime_to_timestamp -from zerver.lib.utils import statsd +from zerver.lib.utils import statsd, get_subdomain, check_subdomain from zerver.exceptions import RateLimited from zerver.lib.rate_limiter import incr_ratelimit, is_ratelimited, \ api_calls_left @@ -279,7 +279,7 @@ def logged_in_and_active(request): return False if request.user.realm.deactivated: return False - return True + return check_subdomain(get_subdomain(request), request.user.realm.subdomain) # Based on Django 1.8's @login_required def zulip_login_required(function=None, diff --git a/zerver/forms.py b/zerver/forms.py index b387d9c724..21e216ad04 100644 --- a/zerver/forms.py +++ b/zerver/forms.py @@ -10,6 +10,8 @@ from django.db.models.query import QuerySet from jinja2 import Markup as mark_safe from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ +from zerver.models import resolve_subdomain_to_realm +from zerver.lib.utils import get_subdomain, check_subdomain import logging @@ -23,6 +25,11 @@ from six import text_type SIGNUP_STRING = u'Your e-mail does not match any existing open organization. ' + \ u'Use a different e-mail address, or contact %s with questions.' % (settings.ZULIP_ADMINISTRATOR,) + +def subdomain_unavailable(subdomain): + # type: (text_type) -> text_type + return u"The subdomain '%s' is not available. Please choose another one." % (subdomain) + if settings.SHOW_OSS_ANNOUNCEMENT: SIGNUP_STRING = u'Your e-mail does not match any existing organization.
' + \ u"The zulip.com service is not taking new customer teams.
" + \ @@ -32,7 +39,10 @@ if settings.SHOW_OSS_ANNOUNCEMENT: MIT_VALIDATION_ERROR = u'That user does not exist at MIT or is a ' + \ u'mailing list. ' + \ u'If you want to sign up an alias for Zulip, ' + \ - u'contact us.' + u'contact us.' +WRONG_SUBDOMAIN_ERROR = "Your Zulip account is not a member of the " + \ + "organization associated with this subdomain. " + \ + "Please contact %s with any questions!" % (settings.ZULIP_ADMINISTRATOR,) def get_registration_string(domain): # type: (text_type) -> text_type @@ -73,10 +83,18 @@ class RegistrationForm(forms.Form): password = forms.CharField(widget=forms.PasswordInput, max_length=100, required=False) realm_name = forms.CharField(max_length=100, required=False) - + realm_subdomain = forms.CharField(max_length=40, required=False) if settings.TERMS_OF_SERVICE: terms = forms.BooleanField(required=True) + def clean_realm_subdomain(self): + # type: () -> str + data = self.cleaned_data['realm_subdomain'] + realm = resolve_subdomain_to_realm(data) + if realm is not None: + raise ValidationError(subdomain_unavailable(data)) + return data + class ToSForm(forms.Form): terms = forms.BooleanField(required=True) @@ -89,8 +107,11 @@ class HomepageForm(forms.Form): def __init__(self, *args, **kwargs): # type: (*Any, **Any) -> None self.domain = kwargs.get("domain") + self.subdomain = kwargs.get("subdomain") if "domain" in kwargs: del kwargs["domain"] + if "subdomain" in kwargs: + del kwargs["subdomain"] super(HomepageForm, self).__init__(*args, **kwargs) def clean_email(self): @@ -106,6 +127,12 @@ class HomepageForm(forms.Form): if completely_open(self.domain): return data + # If the subdomain encodes a complete open realm, pass + subdomain_realm = resolve_subdomain_to_realm(self.subdomain) + if (subdomain_realm is not None and + completely_open(subdomain_realm.domain)): + return data + # If no realm is specified, fail realm = get_valid_realm(data) if realm is None: @@ -191,4 +218,8 @@ Please contact %s to reactivate this group.""" % ( settings.ZULIP_ADMINISTRATOR) raise ValidationError(mark_safe(error_msg)) + if not check_subdomain(get_subdomain(self.request), user_profile.realm.subdomain): + logging.warning("User %s attempted to password login to wrong subdomain %s" % + (user_profile.email, get_subdomain(self.request))) + raise ValidationError(mark_safe(WRONG_SUBDOMAIN_ERROR)) return email diff --git a/zerver/lib/actions.py b/zerver/lib/actions.py index 730d8e7c98..4e218c9369 100644 --- a/zerver/lib/actions.py +++ b/zerver/lib/actions.py @@ -1929,12 +1929,12 @@ def do_change_stream_description(realm, stream_name, new_description): value=new_description) send_event(event, stream_user_ids(stream)) -def do_create_realm(domain, name, restricted_to_domain=True): - # type: (text_type, text_type, bool) -> Tuple[Realm, bool] +def do_create_realm(domain, name, restricted_to_domain=True, subdomain=None): + # type: (text_type, text_type, bool, Optional[text_type]) -> Tuple[Realm, bool] realm = get_realm(domain) created = not realm if created: - realm = Realm(domain=domain, name=name, + realm = Realm(domain=domain, name=name, subdomain=subdomain, restricted_to_domain=restricted_to_domain) realm.save() diff --git a/zerver/lib/test_helpers.py b/zerver/lib/test_helpers.py index 79631af26e..3141380cb8 100644 --- a/zerver/lib/test_helpers.py +++ b/zerver/lib/test_helpers.py @@ -384,18 +384,24 @@ class ZulipTestCase(TestCase): {'email': username + "@" + domain}) return self.submit_reg_form_for_user(username, password, domain=domain) - def submit_reg_form_for_user(self, username, password, domain="zulip.com"): - # type: (text_type, text_type, text_type) -> HttpResponse + def submit_reg_form_for_user(self, username, password, domain="zulip.com", + realm_name=None, realm_subdomain=None, **kwargs): + # type: (text_type, text_type, text_type, Optional[text_type], Optional[text_type], **Any) -> HttpResponse """ Stage two of the two-step registration process. If things are working correctly the account should be fully registered after this call. + + You can pass the HTTP_HOST variable for subdomains via kwargs. """ return self.client_post('/accounts/register/', {'full_name': username, 'password': password, + 'realm_name': realm_name, + 'realm_subdomain': realm_subdomain, 'key': find_key_by_email(username + '@' + domain), - 'terms': True}) + 'terms': True}, + **kwargs) def get_confirmation_url_from_outbox(self, email_address, path_pattern="(\S+)>"): # type: (text_type, text_type) -> text_type diff --git a/zerver/lib/utils.py b/zerver/lib/utils.py index 98629813e3..d8c140a452 100644 --- a/zerver/lib/utils.py +++ b/zerver/lib/utils.py @@ -13,6 +13,7 @@ import os from time import sleep from django.conf import settings +from django.http import HttpRequest from six.moves import range from zerver.lib.str_utils import force_text @@ -184,3 +185,21 @@ def query_chunker(queries, id_collector=None, chunk_size=1000, db_chunk_size=Non id_collector.update(tup_ids) yield [row for row_id, i, row in tup_chunk] + +def get_subdomain(request): + # type: (HttpRequest) -> text_type + domain = request.get_host().lower() + index = domain.find("." + settings.EXTERNAL_HOST) + if index == -1: + return "" + subdomain = domain[0:index] + return subdomain + +def check_subdomain(realm_subdomain, user_subdomain): + # type: (text_type, text_type) -> bool + if settings.REALMS_HAVE_SUBDOMAINS and realm_subdomain is not None: + if (realm_subdomain == "" and user_subdomain is None): + return True + if realm_subdomain != user_subdomain: + return False + return True diff --git a/zerver/middleware.py b/zerver/middleware.py index e812c417e7..d8de1d7b76 100644 --- a/zerver/middleware.py +++ b/zerver/middleware.py @@ -10,16 +10,18 @@ from zerver.lib.response import json_error from zerver.lib.request import JsonableError from django.db import connection from django.http import HttpRequest, HttpResponse -from zerver.lib.utils import statsd +from zerver.lib.utils import statsd, get_subdomain from zerver.lib.queue import queue_json_publish from zerver.lib.cache import get_remote_cache_time, get_remote_cache_requests from zerver.lib.bugdown import get_bugdown_time, get_bugdown_requests -from zerver.models import flush_per_request_caches +from zerver.models import flush_per_request_caches, resolve_subdomain_to_realm from zerver.exceptions import RateLimited from django.contrib.sessions.middleware import SessionMiddleware from django.views.csrf import csrf_failure as html_csrf_failure from django.utils.cache import patch_vary_headers from django.utils.http import cookie_date +from zproject.jinja2 import render_to_response +from django.shortcuts import redirect import logging import time @@ -346,6 +348,17 @@ class FlushDisplayRecipientCache(object): class SessionHostDomainMiddleware(SessionMiddleware): def process_response(self, request, response): # type: (HttpRequest, HttpResponse) -> HttpResponse + if settings.REALMS_HAVE_SUBDOMAINS: + if (not request.path.startswith("/static/") and not request.path.startswith("/api/") + and not request.path.startswith("/json/")): + subdomain = get_subdomain(request) + if (request.get_host() == "127.0.0.1:9991" or request.get_host() == "localhost:9991"): + return redirect("%s%s" % (settings.EXTERNAL_URI_SCHEME, + settings.EXTERNAL_HOST)) + if subdomain != "": + realm = resolve_subdomain_to_realm(subdomain) + if (realm is None): + return render_to_response("zerver/invalid_realm.html") """ If request.session was modified, or if the configuration is to save the session every time, save the changes and set a session cookie. @@ -371,9 +384,15 @@ class SessionHostDomainMiddleware(SessionMiddleware): if response.status_code != 500: request.session.save() host = request.get_host().split(':')[0] + session_cookie_domain = settings.SESSION_COOKIE_DOMAIN - if host.endswith(".e.zulip.com"): - session_cookie_domain = ".e.zulip.com" + # The subdomains feature overrides the + # SESSION_COOKIE_DOMAIN setting, since the setting + # is a fixed value and with subdomains enabled, + # the session cookie domain has to vary with the + # subdomain. + if settings.REALMS_HAVE_SUBDOMAINS: + session_cookie_domain = host response.set_cookie(settings.SESSION_COOKIE_NAME, request.session.session_key, max_age=max_age, expires=expires, domain=session_cookie_domain, diff --git a/zerver/migrations/0029_realm_subdomain.py b/zerver/migrations/0029_realm_subdomain.py new file mode 100644 index 0000000000..b5f4efa1f9 --- /dev/null +++ b/zerver/migrations/0029_realm_subdomain.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.db.backends.postgresql_psycopg2.schema import DatabaseSchemaEditor +from django.db.migrations.state import StateApps +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist + +def set_subdomain_of_default_realm(apps, schema_editor): + # type: (StateApps, DatabaseSchemaEditor) -> None + if settings.DEVELOPMENT: + Realm = apps.get_model('zerver', 'Realm') + try: + default_realm = Realm.objects.get(domain="zulip.com") + except ObjectDoesNotExist: + default_realm = None + + if default_realm is not None: + default_realm.subdomain = "zulip" + default_realm.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('zerver', '0028_userprofile_tos_version'), + ] + + operations = [ + migrations.AddField( + model_name='realm', + name='subdomain', + field=models.CharField(max_length=40, unique=True, null=True), + ), + migrations.RunPython(set_subdomain_of_default_realm) + ] diff --git a/zerver/models.py b/zerver/models.py index 89cb0453b4..54d1840f08 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -140,6 +140,7 @@ class Realm(ModelReprMixin, models.Model): # name is the user-visible identifier for the realm. It has no required # structure. name = models.CharField(max_length=40, null=True) # type: Optional[text_type] + subdomain = models.CharField(max_length=40, null=True, unique=True) # type: Optional[text_type] restricted_to_domain = models.BooleanField(default=True) # type: bool invite_required = models.BooleanField(default=False) # type: bool invite_by_admins_only = models.BooleanField(default=False) # type: bool @@ -199,11 +200,16 @@ class Realm(ModelReprMixin, models.Model): @property def uri(self): # type: () -> str + if settings.REALMS_HAVE_SUBDOMAINS and self.subdomain is not None: + return '%s%s.%s' % (settings.EXTERNAL_URI_SCHEME, + self.subdomain, settings.EXTERNAL_HOST) return settings.SERVER_URI @property def host(self): # type: () -> str + if settings.REALMS_HAVE_SUBDOMAINS and self.subdomain is not None: + return "%s.%s" % (self.subdomain, settings.EXTERNAL_HOST) return settings.EXTERNAL_HOST @property @@ -258,6 +264,13 @@ def resolve_email_to_domain(email): domain = alias.realm.domain return domain +def resolve_subdomain_to_realm(subdomain): + # type: (text_type) -> Optional[Realm] + try: + return Realm.objects.get(subdomain=subdomain) + except Realm.DoesNotExist: + return None + # Is a user with the given email address allowed to be in the given realm? # (This function does not check whether the user has been invited to the realm. # So for invite-only realms, this is the test for whether a user can be invited, diff --git a/zerver/tests/test_integrations.py b/zerver/tests/test_integrations.py index 4bd53dc91b..039d27d106 100644 --- a/zerver/tests/test_integrations.py +++ b/zerver/tests/test_integrations.py @@ -3,7 +3,8 @@ from __future__ import absolute_import import os -from django.test import TestCase +from django.conf import settings +from django.test import TestCase, override_settings from typing import Any from zproject.settings import DEPLOY_ROOT @@ -17,9 +18,27 @@ class IntegrationTest(TestCase): for integration in INTEGRATIONS.values(): self.assertTrue(os.path.isfile(os.path.join(DEPLOY_ROOT, integration.logo))) + @override_settings(REALMS_HAVE_SUBDOMAINS=False) def test_api_url_view_base(self): # type: () -> None context = dict() # type: Dict[str, Any] add_api_uri_context(context, HostRequestMock()) - self.assertEqual(context["external_api_path_subdomain"], "localhost:9991/api") - self.assertEqual(context["external_api_uri_subdomain"], "http://localhost:9991/api") + self.assertEqual(context["external_api_path_subdomain"], "zulipdev.com:9991/api") + self.assertEqual(context["external_api_uri_subdomain"], "http://zulipdev.com:9991/api") + + @override_settings(REALMS_HAVE_SUBDOMAINS=True) + def test_api_url_view_subdomains_base(self): + # type: () -> None + context = dict() # type: Dict[str, Any] + add_api_uri_context(context, HostRequestMock()) + self.assertEqual(context["external_api_path_subdomain"], "yourZulipDomain.zulipdev.com:9991/api") + self.assertEqual(context["external_api_uri_subdomain"], "http://yourZulipDomain.zulipdev.com:9991/api") + + @override_settings(REALMS_HAVE_SUBDOMAINS=True, EXTERNAL_HOST="zulipdev.com") + def test_api_url_view_subdomains_full(self): + # type: () -> None + context = dict() # type: Dict[str, Any] + request = HostRequestMock(host="mysubdomain.zulipdev.com") + add_api_uri_context(context, request) + self.assertEqual(context["external_api_path_subdomain"], "mysubdomain.zulipdev.com:9991/api") + self.assertEqual(context["external_api_uri_subdomain"], "http://mysubdomain.zulipdev.com:9991/api") diff --git a/zerver/tests/test_signup.py b/zerver/tests/test_signup.py index 8b23b0bc78..a1a32282e9 100644 --- a/zerver/tests/test_signup.py +++ b/zerver/tests/test_signup.py @@ -734,3 +734,54 @@ class UserSignUpTest(ZulipTestCase): self.assertEqual(user_profile.default_language, realm.default_language) from django.core.mail import outbox outbox.pop() + + def test_create_realm_with_subdomain(self): + # type: () -> None + username = "user1" + password = "test" + domain = "test.com" + email = "user1@test.com" + subdomain = "test" + realm_name = "Test" + + # Make sure the realm does not exist + self.assertIsNone(get_realm(domain)) + with self.settings(REALMS_HAVE_SUBDOMAINS=True), self.settings(OPEN_REALM_CREATION=True): + # Create new realm with the email + result = self.client_post('/create_realm/', {'email': email}) + self.assertEquals(result.status_code, 302) + self.assertTrue(result["Location"].endswith( + "/accounts/send_confirm/%s@%s" % (username, domain))) + result = self.client_get(result["Location"]) + self.assert_in_response("Check your email so we can get started.", result) + # Visit the confirmation link. + from django.core.mail import outbox + for message in reversed(outbox): + if email in message.to: + confirmation_link_pattern = re.compile(settings.EXTERNAL_HOST + "(\S+)>") + confirmation_url = confirmation_link_pattern.search( + message.body).groups()[0] + break + else: + raise ValueError("Couldn't find a confirmation email.") + + result = self.client_get(confirmation_url) + self.assertEquals(result.status_code, 200) + + result = self.submit_reg_form_for_user(username, + password, + domain=domain, + realm_name=realm_name, + realm_subdomain=subdomain, + # Pass HTTP_HOST for the target subdomain + HTTP_HOST=subdomain + ".testserver") + self.assertEquals(result.status_code, 302) + + # Make sure the realm is created + realm = get_realm(domain) + + self.assertIsNotNone(realm) + self.assertEqual(realm.domain, domain) + self.assertEqual(realm.name, realm_name) + self.assertEqual(realm.subdomain, subdomain) + self.assertEqual(get_user_profile_by_email(email).realm, realm) diff --git a/zerver/views/__init__.py b/zerver/views/__init__.py index 2f225fe5c5..d07da7e428 100644 --- a/zerver/views/__init__.py +++ b/zerver/views/__init__.py @@ -25,7 +25,7 @@ from zerver.models import Message, UserProfile, Stream, Subscription, Huddle, \ get_stream, UserPresence, get_recipient, \ split_email_to_domain, resolve_email_to_domain, email_to_username, get_realm, \ completely_open, get_unique_open_realm, remote_user_to_email, email_allowed_for_realm, \ - get_cross_realm_users + get_cross_realm_users, resolve_subdomain_to_realm from zerver.lib.actions import do_change_password, do_change_full_name, do_change_is_admin, \ do_activate_user, do_create_user, do_create_realm, set_default_streams, \ internal_send_message, update_user_presence, do_events_register, \ @@ -49,7 +49,7 @@ from zerver.lib.avatar import avatar_url from zerver.lib.i18n import get_language_list, get_language_name, \ get_language_list_for_templates from zerver.lib.response import json_success, json_error -from zerver.lib.utils import statsd +from zerver.lib.utils import statsd, get_subdomain from version import ZULIP_VERSION from zproject.backends import password_auth_enabled, dev_auth_enabled, google_auth_enabled @@ -198,7 +198,12 @@ def accounts_register(request): if realm_creation: domain = split_email_to_domain(email) - realm = do_create_realm(domain, form.cleaned_data['realm_name'])[0] + if settings.REALMS_HAVE_SUBDOMAINS: + realm = do_create_realm(domain, form.cleaned_data['realm_name'], + subdomain=form.cleaned_data['realm_subdomain'])[0] + else: + realm = do_create_realm(domain, form.cleaned_data['realm_name'])[0] + set_default_streams(realm, settings.DEFAULT_NEW_REALM_STREAMS) full_name = form.cleaned_data['full_name'] @@ -225,11 +230,21 @@ def accounts_register(request): # This logs you in using the ZulipDummyBackend, since honestly nothing # more fancy than this is required. - login(request, authenticate(username=user_profile.email, use_dummy_backend=True)) + login(request, authenticate(username=user_profile.email, + realm_subdomain=realm.subdomain, + use_dummy_backend=True)) if first_in_realm: do_change_is_admin(user_profile, True) - return HttpResponseRedirect(reverse('zerver.views.initial_invite_page')) + invite_url = reverse('zerver.views.initial_invite_page') + if (realm_creation and settings.REALMS_HAVE_SUBDOMAINS): + invite_url = "%s%s.%s%s" % ( + settings.EXTERNAL_URI_SCHEME, + form.cleaned_data['realm_subdomain'], + settings.EXTERNAL_HOST, + reverse('zerver.views.initial_invite_page') + ) + return HttpResponseRedirect(invite_url) else: return HttpResponseRedirect(reverse('zerver.views.home')) @@ -244,6 +259,7 @@ def accounts_register(request): # but for the registration form, there is no logged in user yet, so # we have to set it here. 'creating_new_team': realm_creation, + 'realms_have_subdomains': settings.REALMS_HAVE_SUBDOMAINS, 'password_auth_enabled': password_auth_enabled(realm), }, request=request) @@ -316,10 +332,11 @@ def get_invitee_emails_set(invitee_emails_raw): def create_homepage_form(request, user_info=None): # type: (HttpRequest, Optional[Dict[str, Any]]) -> HomepageForm if user_info: - return HomepageForm(user_info, domain=request.session.get("domain")) + return HomepageForm(user_info, domain=request.session.get("domain"), + subdomain=get_subdomain(request)) # An empty fields dict is not treated the same way as not # providing it. - return HomepageForm(domain=request.session.get("domain")) + return HomepageForm(domain=request.session.get("domain"), subdomain=get_subdomain(request)) def maybe_send_to_registration(request, email, full_name=''): # type: (HttpRequest, text_type, text_type) -> HttpResponse @@ -362,6 +379,11 @@ def login_or_register_remote_user(request, remote_username, user_profile, full_n return maybe_send_to_registration(request, remote_user_to_email(remote_username), full_name) else: login(request, user_profile) + if settings.OPEN_REALM_CREATION and user_profile.realm.subdomain is not None: + return HttpResponseRedirect("%s%s.%s" % (settings.EXTERNAL_URI_SCHEME, + user_profile.realm.subdomain, + settings.EXTERNAL_HOST)) + return HttpResponseRedirect("%s%s" % (settings.EXTERNAL_URI_SCHEME, request.get_host())) @@ -372,7 +394,7 @@ def remote_user_sso(request): except KeyError: raise JsonableError(_("No REMOTE_USER set.")) - user_profile = authenticate(remote_user=remote_user) + user_profile = authenticate(remote_user=remote_user, realm_subdomain=get_subdomain(request)) return login_or_register_remote_user(request, remote_user, user_profile) @csrf_exempt @@ -401,7 +423,9 @@ def remote_user_jwt(request): # We do all the authentication we need here (otherwise we'd have to # duplicate work), but we need to call authenticate with some backend so # that the request.backend attribute gets set. - user_profile = authenticate(username=email, use_dummy_backend=True) + user_profile = authenticate(username=email, + realm_subdomain=get_subdomain(request), + use_dummy_backend=True) except (jwt.DecodeError, jwt.ExpiredSignature): raise JsonableError(_("Bad JSON web token signature")) except KeyError: @@ -503,7 +527,9 @@ def finish_google_oauth2(request): logging.error('Google oauth2 account email not found: %s' % (body,)) return HttpResponse(status=400) email_address = email['value'] - user_profile = authenticate(username=email_address, use_dummy_backend=True) + user_profile = authenticate(username=email_address, + realm_subdomain=get_subdomain(request), + use_dummy_backend=True) return login_or_register_remote_user(request, email_address, user_profile, full_name) def login_page(request, **kwargs): @@ -536,10 +562,16 @@ def dev_direct_login(request, **kwargs): # This check is probably not required, since authenticate would fail without an enabled DevAuthBackend. raise Exception('Direct login not supported.') email = request.POST['direct_email'] - user_profile = authenticate(username=email) + user_profile = authenticate(username=email, realm_subdomain=get_subdomain(request)) if user_profile is None: raise Exception("User cannot login") login(request, user_profile) + if settings.OPEN_REALM_CREATION and settings.DEVELOPMENT: + if user_profile.realm.subdomain is not None: + return HttpResponseRedirect("%s%s.%s" % (settings.EXTERNAL_URI_SCHEME, + user_profile.realm.subdomain, + settings.EXTERNAL_HOST)) + return HttpResponseRedirect("%s%s" % (settings.EXTERNAL_URI_SCHEME, request.get_host())) @@ -555,7 +587,9 @@ def api_dev_fetch_api_key(request, username=REQ()): if not dev_auth_enabled() or settings.PRODUCTION: return json_error(_("Dev environment not enabled.")) return_data = {} # type: Dict[str, bool] - user_profile = authenticate(username=username, return_data=return_data) + user_profile = authenticate(username=username, + realm_subdomain=get_subdomain(request), + return_data=return_data) if return_data.get("inactive_realm") == True: return json_error(_("Your realm has been deactivated."), data={"reason": "realm deactivated"}, status=403) @@ -660,7 +694,8 @@ def send_registration_completion_email(email, request, realm_creation=False): context = {'support_email': settings.ZULIP_ADMINISTRATOR, 'verbose_support_offers': settings.VERBOSE_SUPPORT_OFFERS} return Confirmation.objects.send_confirmation(prereg_user, email, - additional_context=context) + additional_context=context, + host=request.get_host()) def redirect_to_email_login_url(email): # type: (str) -> HttpResponseRedirect @@ -1003,9 +1038,14 @@ def api_fetch_api_key(request, username=REQ(), password=REQ()): # type: (HttpRequest, str, str) -> HttpResponse return_data = {} # type: Dict[str, bool] if username == "google-oauth2-token": - user_profile = authenticate(google_oauth2_token=password, return_data=return_data) + user_profile = authenticate(google_oauth2_token=password, + realm_subdomain=get_subdomain(request), + return_data=return_data) else: - user_profile = authenticate(username=username, password=password, return_data=return_data) + user_profile = authenticate(username=username, + password=password, + realm_subdomain=get_subdomain(request), + return_data=return_data) if return_data.get("inactive_user") == True: return json_error(_("Your account has been disabled."), data={"reason": "user disable"}, status=403) @@ -1039,7 +1079,8 @@ def api_get_auth_backends(request): def json_fetch_api_key(request, user_profile, password=REQ(default='')): # type: (HttpRequest, UserProfile, str) -> HttpResponse if password_auth_enabled(user_profile.realm): - if not authenticate(username=user_profile.email, password=password): + if not authenticate(username=user_profile.email, password=password, + realm_subdomain=get_subdomain(request)): return json_error(_("Your username or password is incorrect.")) return json_success({"api_key": user_profile.api_key}) diff --git a/zerver/views/integrations.py b/zerver/views/integrations.py index 6361c69294..27ed1a98f9 100644 --- a/zerver/views/integrations.py +++ b/zerver/views/integrations.py @@ -9,15 +9,31 @@ import ujson from zerver.lib import bugdown from zerver.lib.integrations import INTEGRATIONS +from zerver.lib.utils import get_subdomain from zproject.jinja2 import render_to_response def add_api_uri_context(context, request): # type: (Dict[str, Any], HttpRequest) -> None - external_api_path_subdomain = settings.EXTERNAL_API_PATH - external_api_uri_subdomain = settings.EXTERNAL_API_URI + if settings.REALMS_HAVE_SUBDOMAINS: + subdomain = get_subdomain(request) + if subdomain: + display_subdomain = subdomain + html_settings_links = True + else: + display_subdomain = 'yourZulipDomain' + html_settings_links = False + external_api_path_subdomain = '%s.%s' % (display_subdomain, + settings.EXTERNAL_API_PATH) + else: + external_api_path_subdomain = settings.EXTERNAL_API_PATH + html_settings_links = True + + external_api_uri_subdomain = '%s%s' % (settings.EXTERNAL_URI_SCHEME, + external_api_path_subdomain) context['external_api_path_subdomain'] = external_api_path_subdomain context['external_api_uri_subdomain'] = external_api_uri_subdomain + context["html_settings_links"] = html_settings_links class ApiURLView(TemplateView): def get_context_data(self, **kwargs): @@ -26,7 +42,6 @@ class ApiURLView(TemplateView): add_api_uri_context(context, self.request) return context - class APIView(ApiURLView): template_name = 'zerver/api.html' @@ -40,8 +55,12 @@ class IntegrationView(ApiURLView): alphabetical_sorted_integration = OrderedDict(sorted(INTEGRATIONS.items())) context['integrations_dict'] = alphabetical_sorted_integration - settings_html = 'Zulip settings page' - subscriptions_html = 'subscriptions page' + if context["html_settings_links"]: + settings_html = 'Zulip settings page' + subscriptions_html = 'subscriptions page' + else: + settings_html = 'Zulip settings page' + subscriptions_html = 'subscriptions page' context['settings_html'] = settings_html context['subscriptions_html'] = subscriptions_html diff --git a/zilencer/management/commands/populate_db.py b/zilencer/management/commands/populate_db.py index 5c6c2c12be..80ae182b6c 100644 --- a/zilencer/management/commands/populate_db.py +++ b/zilencer/management/commands/populate_db.py @@ -123,7 +123,7 @@ class Command(BaseCommand): clear_database() # Create our two default realms - zulip_realm = Realm.objects.create(domain="zulip.com", name="Zulip Dev") + zulip_realm = Realm.objects.create(domain="zulip.com", name="Zulip Dev", subdomain="zulip") if options["test_suite"]: Realm.objects.create(domain="mit.edu") realms = {} # type: Dict[text_type, Realm] diff --git a/zproject/backends.py b/zproject/backends.py index 16c8ecac9f..1691bf1ea7 100644 --- a/zproject/backends.py +++ b/zproject/backends.py @@ -22,6 +22,7 @@ from social.backends.github import GithubOAuth2, GithubOrganizationOAuth2, \ GithubTeamOAuth2 from social.exceptions import AuthFailed from django.contrib.auth import authenticate +from zerver.lib.utils import check_subdomain def password_auth_enabled(realm): # type: (Realm) -> bool @@ -115,6 +116,11 @@ class SocialAuthMixin(ZulipAuthMixin): return_data["inactive_realm"] = True return None + if not check_subdomain(kwargs.get("realm_subdomain"), + user_profile.realm.subdomain): + return_data["invalid_subdomain"] = True + return None + return user_profile def process_do_auth(self, user_profile, *args, **kwargs): @@ -142,10 +148,14 @@ class ZulipDummyBackend(ZulipAuthMixin): """ Used when we want to log you in but we don't know which backend to use. """ - def authenticate(self, username=None, use_dummy_backend=False): - # type: (Optional[str], bool) -> Optional[UserProfile] + def authenticate(self, username=None, realm_subdomain=None, use_dummy_backend=False): + # type: (Optional[text_type], Optional[text_type], bool) -> Optional[UserProfile] if use_dummy_backend: - return common_get_active_user_by_email(username) + user_profile = common_get_active_user_by_email(username) + if user_profile is None: + return None + if check_subdomain(realm_subdomain, user_profile.realm.subdomain): + return user_profile return None class EmailAuthBackend(ZulipAuthMixin): @@ -155,9 +165,8 @@ class EmailAuthBackend(ZulipAuthMixin): Allows a user to sign in using an email/password pair rather than a username/password pair. """ - - def authenticate(self, username=None, password=None, return_data=None): - # type: (Optional[text_type], Optional[str], Optional[Dict[str, Any]]) -> Optional[UserProfile] + def authenticate(self, username=None, password=None, realm_subdomain=None, return_data=None): + # type: (Optional[text_type], Optional[str], Optional[text_type], Optional[Dict[str, Any]]) -> Optional[UserProfile] """ Authenticate a user based on email address as the user name. """ if username is None or password is None: # Return immediately. Otherwise we will look for a SQL row with @@ -173,7 +182,11 @@ class EmailAuthBackend(ZulipAuthMixin): return_data['password_auth_disabled'] = True return None if user_profile.check_password(password): + if not check_subdomain(realm_subdomain, user_profile.realm.subdomain): + return_data["invalid_subdomain"] = True + return None return user_profile + return None class GoogleMobileOauth2Backend(ZulipAuthMixin): """ @@ -186,8 +199,8 @@ class GoogleMobileOauth2Backend(ZulipAuthMixin): https://developers.google.com/accounts/docs/CrossClientAuth#offlineAccess """ - def authenticate(self, google_oauth2_token=None, return_data=dict()): - # type: (Optional[str], Dict[str, Any]) -> Optional[UserProfile] + def authenticate(self, google_oauth2_token=None, realm_subdomain=None, return_data={}): + # type: (Optional[str], Optional[text_type], Dict[str, Any]) -> Optional[UserProfile] try: token_payload = googleapiclient.verify_id_token(google_oauth2_token, settings.GOOGLE_CLIENT_ID) except AppIdentityError: @@ -204,20 +217,25 @@ class GoogleMobileOauth2Backend(ZulipAuthMixin): if user_profile.realm.deactivated: return_data["inactive_realm"] = True return None + if not check_subdomain(realm_subdomain, user_profile.realm.subdomain): + return_data["invalid_subdomain"] = True + return None return user_profile else: return_data["valid_attestation"] = False class ZulipRemoteUserBackend(RemoteUserBackend): create_unknown_user = False - - def authenticate(self, remote_user): - # type: (str) -> Optional[UserProfile] + def authenticate(self, remote_user, realm_subdomain=None): + # type: (str, Optional[text_type]) -> Optional[UserProfile] if not remote_user: return None email = remote_user_to_email(remote_user) - return common_get_active_user_by_email(email) + user = common_get_active_user_by_email(email) + if user is not None and check_subdomain(realm_subdomain, user.realm.subdomain): + return user + return None class ZulipLDAPException(Exception): pass @@ -257,11 +275,16 @@ class ZulipLDAPAuthBackendBase(ZulipAuthMixin, LDAPBackend): return username class ZulipLDAPAuthBackend(ZulipLDAPAuthBackendBase): - def authenticate(self, username, password, return_data=None): - # type: (text_type, str, Optional[Dict[str, Any]]) -> Optional[str] + def authenticate(self, username, password, realm_subdomain=None, return_data=None): + # type: (text_type, str, Optional[text_type], Optional[Dict[str, Any]]) -> Optional[str] try: username = self.django_to_ldap_username(username) - return ZulipLDAPAuthBackendBase.authenticate(self, username, password) + user_profile = ZulipLDAPAuthBackendBase.authenticate(self, username, password) + if user_profile is None: + return None + if not check_subdomain(realm_subdomain, user_profile.realm.subdomain): + return None + return user_profile except Realm.DoesNotExist: return None except ZulipLDAPException: @@ -292,16 +315,15 @@ class ZulipLDAPAuthBackend(ZulipLDAPAuthBackendBase): # Just like ZulipLDAPAuthBackend, but doesn't let you log in. class ZulipLDAPUserPopulator(ZulipLDAPAuthBackendBase): - def authenticate(self, username, password): - # type: (text_type, str) -> None + def authenticate(self, username, password, realm_subdomain=None): + # type: (text_type, str, Optional[text_type]) -> None return None class DevAuthBackend(ZulipAuthMixin): # Allow logging in as any user without a password. # This is used for convenience when developing Zulip. - - def authenticate(self, username, return_data=None): - # type: (text_type, Optional[Dict[str, Any]]) -> UserProfile + def authenticate(self, username, realm_subdomain=None, return_data=None): + # type: (text_type, Optional[text_type], Optional[Dict[str, Any]]) -> UserProfile return common_get_active_user_by_email(username, return_data=return_data) class GitHubAuthBackend(SocialAuthMixin, GithubOAuth2): diff --git a/zproject/dev_settings.py b/zproject/dev_settings.py index 91f02dccbb..ef047b3dcf 100644 --- a/zproject/dev_settings.py +++ b/zproject/dev_settings.py @@ -4,8 +4,8 @@ from .prod_settings_template import * LOCAL_UPLOADS_DIR = 'var/uploads' -EXTERNAL_HOST = 'localhost:9991' -ALLOWED_HOSTS = ['localhost'] +EXTERNAL_HOST = 'zulipdev.com:9991' +ALLOWED_HOSTS = ['*'] AUTHENTICATION_BACKENDS = ('zproject.backends.DevAuthBackend',) # Add some of the below if you're testing other backends # AUTHENTICATION_BACKENDS = ('zproject.backends.EmailAuthBackend', @@ -20,6 +20,9 @@ EXTRA_INSTALLED_APPS = ["zilencer", "analytics"] # Disable Camo in development CAMO_URI = '' OPEN_REALM_CREATION = True +# Default to subdomains disabled in development until we can update +# the development documentation to make sense with subdomains. +REALMS_HAVE_SUBDOMAINS = False TERMS_OF_SERVICE = 'zproject/terms.md.template' SAVE_FRONTEND_STACKTRACES = True diff --git a/zproject/settings.py b/zproject/settings.py index cdde7af269..35bfec1ba4 100644 --- a/zproject/settings.py +++ b/zproject/settings.py @@ -158,6 +158,7 @@ DEFAULT_SETTINGS = {'TWITTER_CONSUMER_KEY': '', 'VERBOSE_SUPPORT_OFFERS': False, 'STATSD_HOST': '', 'OPEN_REALM_CREATION': False, + 'REALMS_HAVE_SUBDOMAINS': False, 'REMOTE_POSTGRES_HOST': '', 'REMOTE_POSTGRES_SSLMODE': '', # Default GOOGLE_CLIENT_ID to the value needed for Android auth to work diff --git a/zproject/test_settings.py b/zproject/test_settings.py index a46a80e321..340bb6c0c9 100644 --- a/zproject/test_settings.py +++ b/zproject/test_settings.py @@ -82,6 +82,8 @@ LOCAL_UPLOADS_DIR = 'var/test_uploads' S3_KEY = 'test-key' S3_SECRET_KEY = 'test-secret-key' S3_AUTH_UPLOADS_BUCKET = 'test-authed-bucket' +EXTERNAL_HOST = os.getenv('EXTERNAL_HOST', "testserver") +REALMS_HAVE_SUBDOMAINS = bool(os.getenv('REALMS_HAVE_SUBDOMAINS', False)) # Test Custom TOS template rendering TERMS_OF_SERVICE = 'corporate/terms.md'