Skip to content
This repository has been archived by the owner on Jan 2, 2020. It is now read-only.

Commit

Permalink
[#924] Implements update recovery code through bitmask
Browse files Browse the repository at this point in the history
  • Loading branch information
thaissiqueira committed Mar 14, 2017
1 parent 168616b commit 0fe258a
Show file tree
Hide file tree
Showing 12 changed files with 209 additions and 50 deletions.
31 changes: 31 additions & 0 deletions service/pixelated/account_recovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#
# Copyright (c) 2017 ThoughtWorks, Inc.
#
# Pixelated is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Pixelated is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
from twisted.internet.defer import inlineCallbacks, returnValue
import traceback


class AccountRecovery(object):
def __init__(self, session):
self._session = session

@inlineCallbacks
def update_recovery_code(self, recovery_code):
try:
response = yield self._session.update_recovery_code(recovery_code)
returnValue(response)
except Exception as e:
traceback.print_exc(e)
raise
9 changes: 6 additions & 3 deletions service/pixelated/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from twisted.internet import ssl

from pixelated.adapter.welcome_mail import add_welcome_mail
from pixelated.authentication import Authenticator
from pixelated.config import arguments
from pixelated.config import logger
from pixelated.config import services
Expand Down Expand Up @@ -60,7 +61,8 @@ def start_user_agent_in_single_user_mode(root_resource, services_factory, leap_h

services_factory.add_session(leap_session.user_auth.uuid, _services)

root_resource.initialize(provider=leap_session.provider)
authenticator = Authenticator(leap_session.provider)
root_resource.initialize(provider=leap_session.provider, authenticator=authenticator)

# soledad needs lots of threads
reactor.getThreadPool().adjustPoolsize(5, 15)
Expand Down Expand Up @@ -153,14 +155,15 @@ def _setup_multi_user(args, root_resource, services_factory):


def set_up_protected_resources(root_resource, provider, services_factory, banner=None, authenticator=None):
auth = authenticator or Authenticator(provider)
session_checker = SessionChecker(services_factory)

realm = PixelatedRealm()
_portal = portal.Portal(realm, [session_checker, AllowAnonymousAccess()])

anonymous_resource = LoginResource(services_factory, provider, disclaimer_banner=banner, authenticator=authenticator)
anonymous_resource = LoginResource(services_factory, provider, disclaimer_banner=banner, authenticator=auth)
protected_resource = PixelatedAuthSessionWrapper(_portal, root_resource, anonymous_resource, [])
root_resource.initialize(provider, disclaimer_banner=banner, authenticator=authenticator)
root_resource.initialize(provider, disclaimer_banner=banner, authenticator=auth)
return protected_resource


Expand Down
11 changes: 8 additions & 3 deletions service/pixelated/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class Authenticator(object):
def __init__(self, leap_provider):
self._leap_provider = leap_provider
self.domain = leap_provider.server_name
self.bonafide_sesssion = None

@inlineCallbacks
def authenticate(self, username, password):
Expand All @@ -49,9 +50,13 @@ def _srp_auth(self, username, password):
def _bonafide_auth(self, user, password):
srp_provider = Api(self._leap_provider.api_uri)
credentials = Credentials(user, password)
srp_auth = Session(credentials, srp_provider, self._leap_provider.local_ca_crt)
yield srp_auth.authenticate()
returnValue(Authentication(user, srp_auth.token, srp_auth.uuid, 'session_id', {'is_admin': False}))
self.bonafide_sesssion = Session(credentials, srp_provider, self._leap_provider.local_ca_crt)
yield self.bonafide_sesssion.authenticate()
returnValue(Authentication(user,
self.bonafide_sesssion.token,
self.bonafide_sesssion.uuid,
'session_id',
{'is_admin': False}))

def clean_username(self, username):
if '@' not in username:
Expand Down
23 changes: 21 additions & 2 deletions service/pixelated/resources/backup_account_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
from pixelated.resources import BaseResource
from twisted.python.filepath import FilePath
from pixelated.resources import get_protected_static_folder
from twisted.web.http import OK
from pixelated.account_recovery import AccountRecovery
from twisted.web.http import OK, NO_CONTENT, INTERNAL_SERVER_ERROR
from twisted.web.server import NOT_DONE_YET
from twisted.web.template import Element, XMLFile, renderElement


Expand All @@ -34,8 +36,9 @@ def __init__(self):
class BackupAccountResource(BaseResource):
isLeaf = True

def __init__(self, services_factory):
def __init__(self, services_factory, authenticator):
BaseResource.__init__(self, services_factory)
self._authenticator = authenticator

def render_GET(self, request):
request.setResponseCode(OK)
Expand All @@ -44,3 +47,19 @@ def render_GET(self, request):
def _render_template(self, request):
site = BackupAccountPage()
return renderElement(request, site)

def render_POST(self, request):
account_recovery = AccountRecovery(self._authenticator.bonafide_sesssion)

def update_response(response):
request.setResponseCode(NO_CONTENT)
request.finish()

def error_response(response):
request.setResponseCode(INTERNAL_SERVER_ERROR)
request.finish()

d = account_recovery.update_recovery_code("123")
d.addCallbacks(update_response)
d.addErrback(error_response)
return NOT_DONE_YET
2 changes: 1 addition & 1 deletion service/pixelated/resources/login_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def __init__(self, services_factory, provider=None, disclaimer_banner=None, auth
BaseResource.__init__(self, services_factory)
self._disclaimer_banner = disclaimer_banner
self._provider = provider
self._authenticator = authenticator or Authenticator(provider)
self._authenticator = authenticator
self._bootstrap_user_services = BootstrapUserServices(services_factory, provider)

static_folder = get_public_static_folder()
Expand Down
2 changes: 1 addition & 1 deletion service/pixelated/resources/root_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def _is_xsrf_valid(self, request):

def initialize(self, provider=None, disclaimer_banner=None, authenticator=None):
self._child_resources.add('assets', File(self._protected_static_folder))
self._child_resources.add('backup-account', BackupAccountResource(self._services_factory))
self._child_resources.add('backup-account', BackupAccountResource(self._services_factory, authenticator))
self._child_resources.add('sandbox', SandboxResource(self._protected_static_folder))
self._child_resources.add('keys', KeysResource(self._services_factory))
self._child_resources.add(AttachmentsResource.BASE_URL, AttachmentsResource(self._services_factory))
Expand Down
2 changes: 1 addition & 1 deletion service/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ srp==1.0.6
whoosh==2.6.0
Twisted==16.1.1
-e 'git+https://0xacab.org/leap/leap_pycommon.git@master#egg=leap.common'
-e 'git+https://0xacab.org/pixelated/bitmask-dev.git@feat_fetch_remote_on_expiry#egg=leap.bitmask'
-e 'git+https://0xacab.org/pixelated/bitmask-dev.git@recovery-code-and-key-expiry#egg=leap.bitmask'
-e 'git+https://0xacab.org/pixelated/soledad.git@master#egg=leap.soledad.common&subdirectory=common/'
-e 'git+https://0xacab.org/pixelated/soledad.git@master#egg=leap.soledad.client&subdirectory=client/'
-e .
45 changes: 44 additions & 1 deletion service/test/unit/resources/test_backup_account_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from mock import MagicMock, patch
from twisted.trial import unittest
from twisted.web.test.requesthelper import DummyRequest
from twisted.internet import defer

from pixelated.resources.backup_account_resource import BackupAccountResource
from test.unit.resources import DummySite
Expand All @@ -27,7 +28,7 @@
class TestBackupAccountResource(unittest.TestCase):
def setUp(self):
self.services_factory = MagicMock()
self.resource = BackupAccountResource(self.services_factory)
self.resource = BackupAccountResource(self.services_factory, MagicMock())
self.web = DummySite(self.resource)

def test_get(self):
Expand All @@ -41,3 +42,45 @@ def assert_200_when_user_logged_in(_):

d.addCallback(assert_200_when_user_logged_in)
return d

@patch('pixelated.resources.backup_account_resource.AccountRecovery')
def test_post_updates_recovery_code(self, mock_account_recovery_init):
mock_account_recovery = MagicMock()
mock_account_recovery_init.return_value = mock_account_recovery
mock_account_recovery.update_recovery_code.return_value = defer.succeed("Success")
request = DummyRequest(['/backup-account'])
request.method = 'POST'
d = self.web.get(request)

def assert_update_recovery_code_called(_):
mock_account_recovery_init.assert_called_with(self.resource._authenticator.bonafide_sesssion)
mock_account_recovery.update_recovery_code.assert_called()

d.addCallback(assert_update_recovery_code_called)
return d

@patch('pixelated.resources.backup_account_resource.AccountRecovery.update_recovery_code')
def test_post_returns_successfully(self, mock_update_recovery_code):
mock_update_recovery_code.return_value = defer.succeed("Success")
request = DummyRequest(['/backup-account'])
request.method = 'POST'
d = self.web.get(request)

def assert_successful_response(_):
self.assertEqual(204, request.responseCode)

d.addCallback(assert_successful_response)
return d

@patch('pixelated.resources.backup_account_resource.AccountRecovery.update_recovery_code')
def test_post_returns_internal_server_error(self, mock_update_recovery_code):
mock_update_recovery_code.return_value = defer.fail(Exception)
request = DummyRequest(['/backup-account'])
request.method = 'POST'
d = self.web.get(request)

def assert_successful_response(_):
self.assertEqual(500, request.responseCode)

d.addCallback(assert_successful_response)
return d
51 changes: 22 additions & 29 deletions service/test/unit/resources/test_login_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import os

from mock import patch
from mock import patch, MagicMock
from mockito import mock, when, any as ANY
from twisted.cred.error import UnauthorizedLogin
from twisted.internet import defer
Expand Down Expand Up @@ -52,7 +52,7 @@ class TestLoginResource(unittest.TestCase):
def setUp(self):
self.services_factory = mock()
self.portal = mock()
self.resource = LoginResource(self.services_factory, self.portal)
self.resource = LoginResource(self.services_factory, self.portal, authenticator=mock())
self.web = DummySite(self.resource)

def test_children_resources_are_unauthorized_when_not_logged_in(self):
Expand Down Expand Up @@ -170,7 +170,8 @@ class TestLoginPOST(unittest.TestCase):
def setUp(self):
self.services_factory = mock()
self.provider = mock()
self.resource = LoginResource(self.services_factory, self.provider)
self.authenticator = MagicMock()
self.resource = LoginResource(self.services_factory, self.provider, authenticator=self.authenticator)
self.web = DummySite(self.resource)

self.request = DummyRequest([''])
Expand All @@ -185,10 +186,9 @@ def setUp(self):
user_auth.uuid = 'some_user_uuid'
self.user_auth = user_auth

@patch('pixelated.authentication.Authenticator.authenticate')
@patch('twisted.web.util.redirectTo')
@patch('pixelated.resources.session.PixelatedSession.is_logged_in')
def test_should_redirect_to_home_if_user_if_already_logged_in(self, mock_logged_in, mock_redirect, mock_authenticate):
def test_should_redirect_to_home_if_user_if_already_logged_in(self, mock_logged_in, mock_redirect):
mock_logged_in.return_value = True
when(self.services_factory).has_session(ANY()).thenReturn(True)
mock_redirect.return_value = "mocked redirection"
Expand All @@ -197,24 +197,23 @@ def test_should_redirect_to_home_if_user_if_already_logged_in(self, mock_logged_

def assert_redirected_to_home(_):
mock_redirect.assert_called_once_with('/', self.request)
self.assertFalse(mock_authenticate.called)
self.assertFalse(self.authenticator.authenticate.called)

d.addCallback(assert_redirected_to_home)
return d

@patch('pixelated.config.leap.BootstrapUserServices.setup')
@patch('twisted.web.util.redirectTo')
@patch('pixelated.authentication.Authenticator.authenticate')
def test_should_redirect_to_login_with_error_flag_when_login_fails(self, mock_authenticate,
def test_should_redirect_to_login_with_error_flag_when_login_fails(self,
mock_redirect,
mock_user_bootstrap_setup):
mock_authenticate.side_effect = UnauthorizedLogin()
self.authenticator.authenticate.side_effect = UnauthorizedLogin()
mock_redirect.return_value = "mocked redirection"

d = self.web.get(self.request)

def assert_redirected_to_login(_):
mock_authenticate.assert_called_once_with(self.username, self.password)
self.authenticator.authenticate.assert_called_once_with(self.username, self.password)
mock_redirect.assert_called_once_with('/login?auth-error', self.request)
self.assertFalse(mock_user_bootstrap_setup.called)
self.assertFalse(self.resource.get_session(self.request).is_logged_in())
Expand All @@ -223,24 +222,22 @@ def assert_redirected_to_login(_):
return d

@patch('pixelated.config.leap.BootstrapUserServices.setup')
@patch('pixelated.authentication.Authenticator.authenticate')
def test_successful_login_responds_interstitial(self, mock_authenticate, mock_user_bootstrap_setup):
mock_authenticate.return_value = self.user_auth
def test_successful_login_responds_interstitial(self, mock_user_bootstrap_setup):
self.authenticator.authenticate.return_value = self.user_auth

d = self.web.get(self.request)

def assert_interstitial_in_response(_):
mock_authenticate.assert_called_once_with(self.username, self.password)
self.authenticator.authenticate.assert_called_once_with(self.username, self.password)
interstitial_js_in_template = '<script src="/public/interstitial.js"></script>'
self.assertIn(interstitial_js_in_template, self.request.written[0])

d.addCallback(assert_interstitial_in_response)
return d

@patch('pixelated.config.leap.BootstrapUserServices.setup')
@patch('pixelated.authentication.Authenticator.authenticate')
def test_successful_login_runs_user_services_bootstrap_when_interstitial_loaded(self, mock_authenticate, mock_user_bootstrap_setup):
mock_authenticate.return_value = self.user_auth
def test_successful_login_runs_user_services_bootstrap_when_interstitial_loaded(self, mock_user_bootstrap_setup):
self.authenticator.authenticate.return_value = self.user_auth

d = self.web.get(self.request)

Expand All @@ -251,9 +248,8 @@ def assert_login_setup_service_for_user(_):
return d

@patch('pixelated.config.leap.BootstrapUserServices.setup')
@patch('pixelated.authentication.Authenticator.authenticate')
def test_successful_adds_cookies_to_indicate_logged_in_status_when_services_are_loaded(self, mock_authenticate, mock_user_bootstrap_setup):
mock_authenticate.return_value = self.user_auth
def test_successful_adds_cookies_to_indicate_logged_in_status_when_services_are_loaded(self, mock_user_bootstrap_setup):
self.authenticator.authenticate.return_value = self.user_auth
irrelevant = None
mock_user_bootstrap_setup.return_value = defer.succeed(irrelevant)

Expand All @@ -266,9 +262,8 @@ def assert_login_setup_service_for_user(_):
return d

@patch('pixelated.resources.session.PixelatedSession.login_started')
@patch('pixelated.authentication.Authenticator.authenticate')
def test_session_adds_login_started_status_after_authentication(self, mock_authenticate, mock_login_started):
mock_authenticate.return_value = self.user_auth
def test_session_adds_login_started_status_after_authentication(self, mock_login_started):
self.authenticator.authenticate.return_value = self.user_auth

d = self.web.get(self.request)

Expand All @@ -280,9 +275,8 @@ def assert_login_started_called(_):

@patch('pixelated.resources.session.PixelatedSession.login_successful')
@patch('pixelated.config.leap.BootstrapUserServices.setup')
@patch('pixelated.authentication.Authenticator.authenticate')
def test_session_adds_login_successful_status_when_services_setup_finishes(self, mock_authenticate, mock_user_bootstrap_setup, mock_login_successful):
mock_authenticate.return_value = self.user_auth
def test_session_adds_login_successful_status_when_services_setup_finishes(self, mock_user_bootstrap_setup, mock_login_successful):
self.authenticator.authenticate.return_value = self.user_auth
mock_user_bootstrap_setup.return_value = defer.succeed(None)

d = self.web.get(self.request)
Expand All @@ -295,9 +289,8 @@ def assert_login_successful_called(_):

@patch('pixelated.resources.session.PixelatedSession.login_error')
@patch('pixelated.config.leap.BootstrapUserServices.setup')
@patch('pixelated.authentication.Authenticator.authenticate')
def test_session_adds_login_error_status_when_services_setup_gets_error(self, mock_authenticate, mock_user_bootstrap_setup, mock_login_error):
mock_authenticate.return_value = self.user_auth
def test_session_adds_login_error_status_when_services_setup_gets_error(self, mock_user_bootstrap_setup, mock_login_error):
self.authenticator.authenticate.return_value = self.user_auth
mock_user_bootstrap_setup.return_value = defer.fail(Exception('Could not setup user services'))

d = self.web.get(self.request)
Expand Down
Loading

0 comments on commit 0fe258a

Please sign in to comment.