mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 14:03:30 +00:00 
			
		
		
		
	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]
This commit is contained in:
		@@ -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);
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -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');
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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.");
 | 
			
		||||
 
 | 
			
		||||
@@ -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');
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								templates/zerver/invalid_realm.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								templates/zerver/invalid_realm.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
{% extends "zerver/portico.html" %}
 | 
			
		||||
{% block portico_content %}
 | 
			
		||||
 | 
			
		||||
<h3>{{ _('Organization Does Not Exist') }}</h3>
 | 
			
		||||
 | 
			
		||||
<p>{{ _('Hi there! Thank you for your interest in Zulip') }}.</p>
 | 
			
		||||
<p>{% trans %}There is no Zulip organization hosted at this subdomain{% endtrans %}.</p>
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -33,6 +33,7 @@ Form is validated both client-side using jquery-validate (see signup.js) and ser
 | 
			
		||||
                <input type='text' disabled="true" placeholder="{{ email }}" />
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="control-group">
 | 
			
		||||
            <label for="id_full_name" class="control-label">{{ _('Full name') }}</label>
 | 
			
		||||
            <div class="controls">
 | 
			
		||||
@@ -51,7 +52,7 @@ Form is validated both client-side using jquery-validate (see signup.js) and ser
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
      {% if creating_new_team %}
 | 
			
		||||
        {% if creating_new_team %}
 | 
			
		||||
            <div class="control-group">
 | 
			
		||||
                <label for="id_team_name" class="control-label">{{ _('Organization name') }}</label>
 | 
			
		||||
                <div class="controls">
 | 
			
		||||
@@ -65,7 +66,24 @@ Form is validated both client-side using jquery-validate (see signup.js) and ser
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    <br /><span class="small">{{ _('You can change this later on the admin page.') }}</span>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            {% if realms_have_subdomains %}
 | 
			
		||||
            <div class="control-group">
 | 
			
		||||
                <label for="id_team_subdomain" class="control-label">{{ _('Subdomain') }}</label>
 | 
			
		||||
                <div class="controls">
 | 
			
		||||
                    <input id="id_team_subdomain" class="required" type="text"
 | 
			
		||||
                        placeholder="{{ _("E.g. acme") }}"
 | 
			
		||||
                        name="realm_subdomain" maxlength="40" /><b> .{{ external_host }}</b>
 | 
			
		||||
                        {% if form.realm_subdomain.errors %}
 | 
			
		||||
                            {% for error in form.realm_subdomain.errors %}
 | 
			
		||||
                            <div class="alert alert-error">{{ error }}</div>
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    <br /><span class="small"><p>{% trans %}The address you'll use to sign in to your organization.{% endtrans %}.</p></span>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
 | 
			
		||||
        {% endif %}
 | 
			
		||||
 | 
			
		||||
        {% if password_auth_enabled %}
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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. <br />' + \
 | 
			
		||||
                    u"The zulip.com service is not taking new customer teams. <br /> " + \
 | 
			
		||||
@@ -32,7 +39,10 @@ if settings.SHOW_OSS_ANNOUNCEMENT:
 | 
			
		||||
MIT_VALIDATION_ERROR = u'That user does not exist at MIT or is a ' + \
 | 
			
		||||
                       u'<a href="https://ist.mit.edu/email-lists">mailing list</a>. ' + \
 | 
			
		||||
                       u'If you want to sign up an alias for Zulip, ' + \
 | 
			
		||||
                       u'<a href="mailto:"' + settings.ZULIP_ADMINISTRATOR + '">contact us</a>.'
 | 
			
		||||
                       u'<a href="mailto:support@zulipchat.com">contact us</a>.'
 | 
			
		||||
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
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										36
									
								
								zerver/migrations/0029_realm_subdomain.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								zerver/migrations/0029_realm_subdomain.py
									
									
									
									
									
										Normal file
									
								
							@@ -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)
 | 
			
		||||
    ]
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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")
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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})
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 = '<a href="../#settings">Zulip settings page</a>'
 | 
			
		||||
        subscriptions_html = '<a target="_blank" href="../#subscriptions">subscriptions page</a>'
 | 
			
		||||
        if context["html_settings_links"]:
 | 
			
		||||
            settings_html = '<a href="../#settings">Zulip settings page</a>'
 | 
			
		||||
            subscriptions_html = '<a target="_blank" href="../#subscriptions">subscriptions page</a>'
 | 
			
		||||
        else:
 | 
			
		||||
            settings_html = 'Zulip settings page'
 | 
			
		||||
            subscriptions_html = 'subscriptions page'
 | 
			
		||||
 | 
			
		||||
        context['settings_html'] = settings_html
 | 
			
		||||
        context['subscriptions_html'] = subscriptions_html
 | 
			
		||||
 
 | 
			
		||||
@@ -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]
 | 
			
		||||
 
 | 
			
		||||
@@ -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):
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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'
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user