Skip to content

Commit

Permalink
Merge 904ad82 into 6e1674a
Browse files Browse the repository at this point in the history
  • Loading branch information
umairwaheed committed Oct 21, 2016
2 parents 6e1674a + 904ad82 commit 279b90f
Show file tree
Hide file tree
Showing 17 changed files with 273 additions and 47 deletions.
51 changes: 51 additions & 0 deletions django_templates/two_factor/_base.html
@@ -0,0 +1,51 @@
{% load pipeline %}

<!DOCTYPE html>
<html>
<head>
<title>{% block title %}{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!--[if lt IE 9]>
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7/html5shiv.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/respond.js/1.3.0/respond.js"></script>
<![endif]-->
{% stylesheet 'common' %}
{% stylesheet 'portico' %}
</head>
<body>
{% block content_wrapper %}
<div class="header">
<div class="header-main" id="top_navbar">
<div class="column-left">
<div>
{% if custom_logo_url %}
<a class="brand logo" href="{{ server_uri }}/"><img src="{{ custom_logo_url }}" class="portico-logo" alt="Zulip" content="Zulip" /></a>
{% else %}
<a class="brand logo" href="{{ server_uri }}/"><img src="/static/images/logo/zulipcornerlogo@2x.png" class="portico-simple-logo" alt="Zulip" content="Zulip" /></a>
{% endif %}
</div>
</div>

<div class="column-right top-links">
<a href='/#settings'>Back to Settings</a>
</div>
</div>
</div>
<div class="app portico-page">
<div class="portico-os-announcement">
{% block os_announcement %}
{% endblock %}
</div>
<div class="app-main portico-page-container{% block hello_page_container %}{% endblock %}">
{% block content %}{% endblock %}
</div>
<div class="footer-padder{% block hello_page_footer %}{% endblock %}"></div>
</div>

{% endblock %}

<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.0.2/js/bootstrap.min.js"></script>
</body>
</html>

32 changes: 32 additions & 0 deletions django_templates/two_factor/_base_focus.html
@@ -0,0 +1,32 @@
{% extends "two_factor/_base.html" %}

{% block content_wrapper %}
<div class="header">
<div class="header-main" id="top_navbar">
<div class="column-left">
<div>
{% if custom_logo_url %}
<a class="brand logo" href="{{ server_uri }}/"><img src="{{ custom_logo_url }}" class="portico-logo" alt="Zulip" content="Zulip" /></a>
{% else %}
<a class="brand logo" href="{{ server_uri }}/"><img src="/static/images/logo/zulipcornerlogo@2x.png" class="portico-simple-logo" alt="Zulip" content="Zulip" /></a>
{% endif %}
</div>
</div>

<div class="column-right top-links">
<a href='/#settings'>Back to Settings</a>
</div>
</div>
</div>
<div class="app portico-page">
<div class="portico-os-announcement">
{% block os_announcement %}
{% endblock %}
</div>
<div class="app-main portico-page-container{% block hello_page_container %}{% endblock %}">
{% block content %}{% endblock %}
</div>
<div class="footer-padder{% block hello_page_footer %}{% endblock %}"></div>
</div>

{% endblock %}
8 changes: 4 additions & 4 deletions frontend_tests/casper_lib/common.js
Expand Up @@ -18,9 +18,9 @@ function log_in(credentials) {
}

casper.test.info('Logging in');
casper.fill('form[action^="/accounts/login"]', {
username: credentials.username,
password: credentials.password
casper.fill('form#login_form', {
'auth-username': credentials.username,
'auth-password': credentials.password
}, true /* submit form */);
}

Expand Down Expand Up @@ -114,7 +114,7 @@ exports.then_log_out = function () {

});
casper.waitUntilVisible(".login-page-header", function () {
casper.test.assertUrlMatch(/accounts\/login\/$/);
casper.test.assertUrlMatch(/\/login\/$/);
casper.test.info("Logged out");
});
};
Expand Down
3 changes: 3 additions & 0 deletions requirements/common.txt
Expand Up @@ -159,3 +159,6 @@ git+https://github.com/lorenzogil/glue@01c00cd33b9b78ea868300c266c16acd59a81bfc#

# Needed for cloning virtual environments
git+https://github.com/umairwaheed/virtualenv-clone.git@short-version#egg=virtualenv-clone==0.2.6

# Needed for Two Factor Authentication
-r two-factor-auth.txt
8 changes: 8 additions & 0 deletions requirements/two-factor-auth.txt
@@ -0,0 +1,8 @@
django-otp==0.3.5
django-phonenumber-field==1.1.0
django-two-factor-auth==1.4.0
phonenumbers==7.5.2
qrcode==5.3
twilio==5.4.0
django-formtools==1.0
pysocks==1.5.7
4 changes: 4 additions & 0 deletions static/templates/settings/account-settings.handlebars
Expand Up @@ -13,6 +13,10 @@

<!-- password start -->
{{#if page_params.password_auth_enabled}}
<div class="input-group">
<label for="enable_2fa">{{t "Security settings" }}</label>
<a href="/account/two_factor" class="btn btn-big btn-primary" name="enable_2fa">Manage 2-factor authentication</a>
</div>
<div class="input-group" id="pw_change_link">
<label for="change_password_button">{{t "Password" }}</label>
<button class="change_password_button button" data-dismiss="modal" aria-hidden="true">{{t "Change Password" }}</button>
Expand Down
1 change: 1 addition & 0 deletions static/templates/settings_tab.handlebars
Expand Up @@ -9,6 +9,7 @@
{{ partial "display-settings" }}

{{ partial "notification-settings" }}

</div>

{{ partial "bot-settings" }}
Expand Down
9 changes: 7 additions & 2 deletions templates/zerver/login.html
Expand Up @@ -92,14 +92,19 @@ <h3 class="login-page-header">{{ _('You look familiar.') }}</h3>
{# desktop_sso_dispatch is only set when this template is invoked by zilencer.views #}
action="{{ url('zilencer.views.account_deployment_dispatch') }}"
{% else %}
{% if next %}
action="{{ url('django.contrib.auth.views.login') }}?next={{ next }}"
{% else %}
action="{{ url('django.contrib.auth.views.login') }}"
{% endif %}
{% endif %}
>
{{ wizard.management_form }}
{{ csrf_input }}
<div class="control-group">
<label for="id_username" class="control-label">{{ _('Email') }}</label>
<div class="controls">
<input id="id_username" type="email" name="username"
<input id="id_username" type="email" name="auth-username"
class="email required"
{% if email %}
value="{{ email }}"
Expand All @@ -114,7 +119,7 @@ <h3 class="login-page-header">{{ _('You look familiar.') }}</h3>
<div class="control-group">
<label for="id_password" class="control-label">{{ _('Password') }}</label>
<div class="controls">
<input id="id_password" name="password" class="required" type="password" />
<input id="id_password" name="auth-password" class="required" type="password" />
</div>
</div>
{% endif %}
Expand Down
4 changes: 2 additions & 2 deletions zerver/lib/logging_util.py
Expand Up @@ -29,8 +29,8 @@ def filter(self, record):
use_cache = False

if use_cache:
tb = force_bytes('\n'.join(traceback.format_exception(*record.exc_info)))
key = self.__class__.__name__.upper() + hashlib.sha1(tb).hexdigest()
tb = u'\n'.join(traceback.format_exception(*record.exc_info))
key = self.__class__.__name__.upper() + hashlib.sha1(tb.encode('utf8')).hexdigest()
duplicate = cache.get(key) == 1
if not duplicate:
cache.set(key, 1, rate)
Expand Down
7 changes: 5 additions & 2 deletions zerver/lib/test_helpers.py
Expand Up @@ -4,6 +4,7 @@
from typing import (cast, Any, Callable, Dict, Generator, Iterable, List, Mapping, Optional,
Sized, Tuple, Union)

from django.core.urlresolvers import reverse
from django.conf import settings
from django.test import TestCase
from django.test.client import (
Expand Down Expand Up @@ -366,8 +367,10 @@ def login_with_return(self, email, password=None):
# type: (text_type, Optional[text_type]) -> HttpResponse
if password is None:
password = initial_password(email)
return self.client_post('/accounts/login/',
{'username': email, 'password': password})
return self.client_post(reverse('django.contrib.auth.views.login'),
{'auth-username': email,
'auth-password': password,
'login_view-current_step': 'auth'})

def login(self, email, password=None, fails=False):
# type: (text_type, Optional[text_type], bool) -> HttpResponse
Expand Down
85 changes: 78 additions & 7 deletions zerver/tests/test_signup.py
@@ -1,8 +1,10 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from typing import Union, Tuple, Any, Dict
from django.conf import settings
from django.http import HttpResponse
from django.test import TestCase
from mock import patch, MagicMock

from zilencer.models import Deployment

Expand All @@ -26,12 +28,14 @@
from zerver.lib.test_helpers import ZulipTestCase, find_key_by_email, queries_captured
from zerver.lib.test_runner import slow
from zerver.lib.session_user import get_session_dict_user
from two_factor.models import PhoneDevice
from two_factor.gateways import send_sms

import re
import ujson

from six.moves import urllib
from six.moves import range
from six.moves import range, urllib_parse
import six
from six import text_type

Expand All @@ -42,9 +46,13 @@ class PublicURLTest(ZulipTestCase):
"""

def fetch(self, method, urls, expected_status):
# type: (str, List[str], int) -> None
# type: (str, List[Union[str, Tuple[str, Dict[str, Any]]]], int) -> None
for url in urls:
response = getattr(self.client, method)(url) # e.g. self.client_post(url) if method is "post"
args = {} # type: Dict[str, Any]
if isinstance(url, tuple):
url, args = url # type: ignore # Gives error: 'builtins.None' object is not iterable

response = getattr(self.client, method)(url, args) # e.g. self.client_post(url) if method is "post"
self.assertEqual(response.status_code, expected_status,
msg="Expected %d, received %d for %s to %s" % (
expected_status, response.status_code, method, url))
Expand All @@ -67,8 +75,8 @@ def test_public_urls(self):
"/json/messages",
"/json/streams",
],
}
post_urls = {200: ["/accounts/login/"],
} # type: Dict[int, List[Union[str, Tuple[str, Dict[str, Any]]]]]
post_urls = {200: [("/accounts/login/", {'login_view-current_step': 'auth'})],
302: ["/accounts/logout/"],
401: ["/json/messages",
"/json/invite_users",
Expand All @@ -84,9 +92,9 @@ def test_public_urls(self):
400: ["/api/v1/external/github",
"/api/v1/fetch_api_key",
],
}
} # type: Dict[int, List[Union[str, Tuple[str, Dict[str, Any]]]]]
put_urls = {401: ["/json/users/me/pointer"],
}
} # type: Dict[int, List[Union[str, Tuple[str, Dict[str, Any]]]]]
for status_code, url_set in six.iteritems(get_urls):
self.fetch("get", url_set, status_code)
for status_code, url_set in six.iteritems(post_urls):
Expand Down Expand Up @@ -782,3 +790,66 @@ def test_do_not_deactivate_final_admin(self):
result = self.client_delete('/json/users/me')
self.assert_json_success(result)
do_change_is_admin(user, True)

class TwoFactorAuthTest(ZulipTestCase):
@patch('two_factor.models.totp')
def test_two_factor_login(self, mock_totp):
# type: (MagicMock) -> None
token = 123456
email = 'hamlet@zulip.com'
password = 'testing'
number = "+12223334444"

user_profile = get_user_profile_by_email(email)
user_profile.set_password(password)
user_profile.save()
phone_device = PhoneDevice(user=user_profile, name='default',
confirmed=True, number=number,
key='abcd', method='sms')
phone_device.save()

def totp(*args, **kwargs):
# type: (*Any, **Any) -> int
return token

mock_totp.side_effect = totp

with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.EmailAuthBackend',),
TWO_FACTOR_CALL_GATEWAY='two_factor.gateways.fake.Fake',
TWO_FACTOR_SMS_GATEWAY='two_factor.gateways.fake.Fake'):

first_step_data = {"auth-username": email,
"auth-password": password,
"login_view-current_step": "auth"}
result = self.client_post("/account/login/", first_step_data)
self.assertEqual(result.status_code, 200)

second_step_data = {"token-otp_token": str(token),
"login_view-current_step": "token"}
result = self.client_post("/account/login/", second_step_data)
self.assertEqual(result.status_code, 302)
location = result['Location']
result = urllib_parse.urlparse(location)
self.assertEqual(result.path, '/')

def test_two_factor_disabled_login(self):
# type: () -> None
email = 'hamlet@zulip.com'
password = 'testing'

user_profile = get_user_profile_by_email(email)
user_profile.set_password(password)
user_profile.save()

with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.EmailAuthBackend',),
TWO_FACTOR_CALL_GATEWAY='two_factor.gateways.fake.Fake',
TWO_FACTOR_SMS_GATEWAY='two_factor.gateways.fake.Fake'):

data = {"auth-username": email,
"auth-password": password,
"login_view-current_step": "auth"}
result = self.client_post("/account/login/", data)
self.assertEqual(result.status_code, 302)
location = result['Location']
result = urllib_parse.urlparse(location)
self.assertEqual(result.path, '/')
4 changes: 2 additions & 2 deletions zerver/tests/test_subs.py
Expand Up @@ -1526,7 +1526,7 @@ def test_bulk_subscribe_MIT(self):
# Make sure Zephyr mirroring realms such as MIT do not get
# any tornado subscription events
self.assert_length(events, 0)
self.assert_max_length(queries, 7)
self.assert_max_length(queries, 8)

def test_bulk_subscribe_many(self):
# type: () -> None
Expand All @@ -1544,7 +1544,7 @@ def test_bulk_subscribe_many(self):
dict(principals=ujson.dumps([self.test_email])),
)
# Make sure we don't make O(streams) queries
self.assert_max_length(queries, 9)
self.assert_max_length(queries, 10)

@slow("common_subscribe_to_streams is slow")
def test_subscriptions_add_for_principal(self):
Expand Down
4 changes: 4 additions & 0 deletions zerver/views/__init__.py
Expand Up @@ -15,6 +15,7 @@
from django.core.exceptions import ValidationError
from django.core import validators
from django.core.mail import send_mail
from django_otp.decorators import otp_required
from zerver.models import Message, UserProfile, Stream, Subscription, Huddle, \
Recipient, Realm, UserMessage, DefaultStream, RealmEmoji, RealmAlias, \
RealmFilter, \
Expand Down Expand Up @@ -250,6 +251,7 @@ def accounts_register(request):
request=request)

@zulip_login_required
@otp_required(login_url = settings.HOME_NOT_LOGGED_IN, if_configured=True)
def accounts_accept_terms(request):
# type: (HttpRequest) -> HttpResponse
if request.method == "POST":
Expand Down Expand Up @@ -415,6 +417,7 @@ def sent_time_in_epoch_seconds(user_message):
return calendar.timegm(user_message.message.pub_date.utctimetuple())

@zulip_login_required
@otp_required(login_url = settings.HOME_NOT_LOGGED_IN, if_configured=True)
def home(request):
# type: (HttpRequest) -> HttpResponse
# We need to modify the session object every two weeks or it will expire.
Expand Down Expand Up @@ -639,6 +642,7 @@ def home(request):
return response

@zulip_login_required
@otp_required(login_url = settings.HOME_NOT_LOGGED_IN, if_configured=True)
def desktop_home(request):
# type: (HttpRequest) -> HttpResponse
return HttpResponseRedirect(reverse('zerver.views.home'))
Expand Down

0 comments on commit 279b90f

Please sign in to comment.