diff --git a/confirmation/migrations/0002_realmcreationkey.py b/confirmation/migrations/0002_realmcreationkey.py new file mode 100644 index 0000000000..fdebfdbb30 --- /dev/null +++ b/confirmation/migrations/0002_realmcreationkey.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('confirmation', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='RealmCreationKey', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('creation_key', models.CharField(max_length=40, verbose_name='activation key')), + ('date_created', models.DateTimeField(default=django.utils.timezone.now, verbose_name='created')), + ], + ), + ] diff --git a/confirmation/models.py b/confirmation/models.py index 9c291f7aae..b30857d438 100644 --- a/confirmation/models.py +++ b/confirmation/models.py @@ -30,6 +30,15 @@ except ImportError: B16_RE = re.compile('^[a-f0-9]{40}$') +def check_key_is_valid(creation_key): + if not RealmCreationKey.objects.filter(creation_key=creation_key).exists(): + return False + days_sofar = (now() - RealmCreationKey.objects.get(creation_key=creation_key).date_created).days + # Realm creation link expires after settings.REALM_CREATION_LINK_VALIDITY_DAYS + if days_sofar <= settings.REALM_CREATION_LINK_VALIDITY_DAYS: + return True + return False + def generate_key(): return generate_random_token(40) @@ -39,6 +48,13 @@ def generate_activation_url(key): reverse('confirmation.views.confirm', kwargs={'confirmation_key': key})) +def generate_realm_creation_url(): + key = generate_key() + RealmCreationKey.objects.create(creation_key=key, date_created=now()) + return u'%s%s%s' % (settings.EXTERNAL_URI_SCHEME, + settings.EXTERNAL_HOST, + reverse('zerver.views.create_realm', + kwargs={'creation_key': key})) class ConfirmationManager(models.Manager): @@ -111,3 +127,7 @@ class Confirmation(models.Model): def __unicode__(self): return _('confirmation email for %s') % (self.content_object,) + +class RealmCreationKey(models.Model): + creation_key = models.CharField(_('activation key'), max_length=40) + date_created = models.DateTimeField(_('created'), default=now) diff --git a/zerver/management/commands/generate_realm_creation_link.py b/zerver/management/commands/generate_realm_creation_link.py new file mode 100644 index 0000000000..c7370dd44a --- /dev/null +++ b/zerver/management/commands/generate_realm_creation_link.py @@ -0,0 +1,21 @@ +from __future__ import absolute_import +from __future__ import print_function + +from typing import Any +from django.conf import settings +from django.core.management.base import BaseCommand +from confirmation.models import generate_realm_creation_url + +class Command(BaseCommand): + help = """Outputs a randomly generated, 1-time-use link for Organization creation. + Whoever visits the link can create a new organization on this server, regardless of whether + settings.OPEN_REALM_CREATION is enabled. The link would expire automatically after + settings.REALM_CREATION_LINK_VALIDITY_DAYS. + + Usage: python manage.py generate_realm_creation_link """ + + def handle(self, *args, **options): + # type: (*Any, **Any) -> None + url = generate_realm_creation_url() + self.stdout.write("\033[1;92mOne time organization creation link generated\033[0m") + self.stdout.write("\033[1;92m=> Please visit \033[4m%s\033[0m \033[1;92mto create the organization\033[0m" % (url)) diff --git a/zerver/tests/test_management_commands.py b/zerver/tests/test_management_commands.py index 2691917bbb..d9c75b1711 100644 --- a/zerver/tests/test_management_commands.py +++ b/zerver/tests/test_management_commands.py @@ -4,7 +4,9 @@ from mock import patch from django.test import TestCase from django.conf import settings from django.core.management import call_command - +from zerver.models import get_realm +from confirmation.models import RealmCreationKey, generate_realm_creation_url +from datetime import timedelta class TestSendWebhookFixtureMessage(TestCase): COMMAND_NAME = 'send_webhook_fixture_message' @@ -57,3 +59,53 @@ class TestSendWebhookFixtureMessage(TestCase): self.assertTrue(ujson_mock.loads.called) self.assertTrue(open_mock.called) client.post.assert_called_once_with(self.url, {}, content_type="application/json") + +class TestGenerateRealmCreationLink(TestCase): + COMMAND_NAME = "generate_realm_creation_link" + + def test_generate_link_and_create_realm(self): + username = "user1" + domain = "test.com" + email = "user1@test.com" + generated_link = generate_realm_creation_url() + + with self.settings(OPEN_REALM_CREATION=False): + # Check realm creation page is accessible + result = self.client.get(generated_link) + self.assertEquals(result.status_code, 200) + self.assertIn("Let's get started…", result.content) + + # Create Realm with generated link + self.assertIsNone(get_realm(domain)) + result = self.client.post(generated_link, {'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.assertIn("Check your email so we can get started.", result.content) + + # Generated link used for creating realm + result = self.client.get(generated_link) + self.assertEquals(result.status_code, 200) + self.assertIn("The organization creation link has been expired or is not valid.", result.content) + + def test_realm_creation_with_random_link(self): + with self.settings(OPEN_REALM_CREATION=False): + # Realm creation attempt with an invalid link should fail + random_link = "/create_realm/5e89081eb13984e0f3b130bf7a4121d153f1614b" + result = self.client.get(random_link) + self.assertEquals(result.status_code, 200) + self.assertIn("The organization creation link has been expired or is not valid.", result.content) + + def test_realm_creation_with_expired_link(self): + with self.settings(OPEN_REALM_CREATION=False): + generated_link = generate_realm_creation_url() + key = generated_link[-40:] + # Manually expire the link by changing the date of creation + obj = RealmCreationKey.objects.get(creation_key=key) + obj.date_created = obj.date_created - timedelta(days=settings.REALM_CREATION_LINK_VALIDITY_DAYS + 1) + obj.save() + + result = self.client.get(generated_link) + self.assertEquals(result.status_code, 200) + self.assertIn("The organization creation link has been expired or is not valid.", result.content) diff --git a/zerver/views/__init__.py b/zerver/views/__init__.py index b6f1153e40..d8cc59e230 100644 --- a/zerver/views/__init__.py +++ b/zerver/views/__init__.py @@ -50,7 +50,7 @@ from zerver.lib.response import json_success, json_error from zerver.lib.utils import statsd, generate_random_token from zproject.backends import password_auth_enabled, dev_auth_enabled, google_auth_enabled -from confirmation.models import Confirmation +from confirmation.models import Confirmation, RealmCreationKey, check_key_is_valid import requests @@ -117,9 +117,6 @@ def accounts_register(request): domain = resolve_email_to_domain(email) realm = get_realm(domain) - if realm_creation and not settings.OPEN_REALM_CREATION: - return render_to_response("zerver/realm_creation_failed.html", {'message': _('New organization creation disabled.')}) - if realm and realm.deactivated: # The user is trying to register for a deactivated realm. Advise them to # contact support. @@ -727,10 +724,13 @@ When settings.OPEN_REALM_CREATION is enabled public users can create new realm. not be the member of any current realm. The realm is created with domain same as the that of the user's email. When there is no unique_open_realm user registrations are made by visiting /register/domain_of_the_realm. """ -def create_realm(request): - # type: (HttpRequest) -> HttpResponse +def create_realm(request, creation_key=None): + # type: (HttpRequest, Optional[text_type]) -> HttpResponse if not settings.OPEN_REALM_CREATION: - return render_to_response("zerver/realm_creation_failed.html", {'message': _('New organization creation disabled.')}) + if creation_key is None: + return render_to_response("zerver/realm_creation_failed.html", {'message': _('New organization creation disabled.')}) + elif not check_key_is_valid(creation_key): + return render_to_response("zerver/realm_creation_failed.html", {'message': _('The organization creation link has been expired or is not valid.')}) if request.method == 'POST': form = RealmCreationForm(request.POST, domain=request.session.get("domain")) @@ -739,6 +739,8 @@ def create_realm(request): confirmation_key = send_registration_completion_email(email, request, realm_creation=True).confirmation_key if settings.DEVELOPMENT: request.session['confirmation_key'] = {'confirmation_key': confirmation_key} + if (creation_key is not None and check_key_is_valid(creation_key)): + RealmCreationKey.objects.get(creation_key=creation_key).delete() return HttpResponseRedirect(reverse('send_confirm', kwargs={'email': email})) try: email = request.POST['email'] diff --git a/zproject/settings.py b/zproject/settings.py index a6ea7cf6df..896cdb88f5 100644 --- a/zproject/settings.py +++ b/zproject/settings.py @@ -149,6 +149,7 @@ DEFAULT_SETTINGS = {'TWITTER_CONSUMER_KEY': '', 'DBX_APNS_CERT_FILE': None, 'EXTRA_INSTALLED_APPS': [], 'DEFAULT_NEW_REALM_STREAMS': ["social", "general", "zulip"], + 'REALM_CREATION_LINK_VALIDITY_DAYS': 7, } for setting_name, setting_val in six.iteritems(DEFAULT_SETTINGS): diff --git a/zproject/urls.py b/zproject/urls.py index 9e36a9f48c..ba407ad8e0 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -81,6 +81,7 @@ i18n_urls = [ # Realm Creation url(r'^create_realm/$', 'zerver.views.create_realm'), + url(r'^create_realm/(?P[\w]+)$', 'zerver.views.create_realm'), # Login/registration url(r'^register/$', 'zerver.views.accounts_home', name='register'),