diff options
-rw-r--r-- | service/pixelated/application.py | 9 | ||||
-rw-r--r-- | service/pixelated/authentication.py | 63 | ||||
-rw-r--r-- | service/pixelated/config/leap.py | 61 | ||||
-rw-r--r-- | service/pixelated/config/services.py | 1 | ||||
-rw-r--r-- | service/pixelated/register.py | 2 | ||||
-rw-r--r-- | service/pixelated/resources/login_resource.py | 61 | ||||
-rw-r--r-- | service/pixelated/resources/root_resource.py | 11 | ||||
-rw-r--r-- | service/test/integration/test_logout.py | 1 | ||||
-rw-r--r-- | service/test/integration/test_multi_user_login.py | 1 | ||||
-rw-r--r-- | service/test/support/integration/app_test_client.py | 31 | ||||
-rw-r--r-- | service/test/support/integration/multi_user_client.py | 13 | ||||
-rw-r--r-- | service/test/unit/config/test_leap.py | 70 | ||||
-rw-r--r-- | service/test/unit/config/test_services.py | 58 | ||||
-rw-r--r-- | service/test/unit/resources/test_login_resource.py | 109 | ||||
-rw-r--r-- | service/test/unit/test_authentication.py | 50 |
15 files changed, 378 insertions, 163 deletions
diff --git a/service/pixelated/application.py b/service/pixelated/application.py index d4d8c280..d393b656 100644 --- a/service/pixelated/application.py +++ b/service/pixelated/application.py @@ -152,17 +152,16 @@ def _setup_multi_user(args, root_resource, services_factory): return protected_resource -def set_up_protected_resources(root_resource, provider, services_factory, checker=None, banner=None): - if not checker: - checker = LeapPasswordChecker(provider) +def set_up_protected_resources(root_resource, provider, services_factory, checker=None, banner=None, authenticator=None): + checker = checker or LeapPasswordChecker(provider) session_checker = SessionChecker(services_factory) realm = PixelatedRealm() _portal = portal.Portal(realm, [checker, session_checker, AllowAnonymousAccess()]) - anonymous_resource = LoginResource(services_factory, _portal, disclaimer_banner=banner) + anonymous_resource = LoginResource(services_factory, provider, disclaimer_banner=banner, authenticator=authenticator) protected_resource = PixelatedAuthSessionWrapper(_portal, root_resource, anonymous_resource, []) - root_resource.initialize(_portal, disclaimer_banner=banner) + root_resource.initialize(provider, disclaimer_banner=banner, authenticator=authenticator) return protected_resource diff --git a/service/pixelated/authentication.py b/service/pixelated/authentication.py index 02b43a1e..aa1d8b5d 100644 --- a/service/pixelated/authentication.py +++ b/service/pixelated/authentication.py @@ -1,9 +1,29 @@ +# +# Copyright (c) 2014 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/>. import re -from pixelated.config.leap import authenticate +from collections import namedtuple + +from leap.bitmask.bonafide.provider import Api +from leap.bitmask.bonafide.session import Session from leap.bitmask.bonafide._srp import SRPAuthError from twisted.cred.error import UnauthorizedLogin -from twisted.internet.defer import inlineCallbacks +from twisted.internet.defer import inlineCallbacks, returnValue + +Credentials = namedtuple('Credentials', 'username, password') class Authenticator(object): @@ -13,27 +33,48 @@ class Authenticator(object): @inlineCallbacks def authenticate(self, username, password): - if self.validate_username(username): - yield self._srp_auth(username, password) - else: - raise UnauthorizedLogin() + username = self.clean_username(username) + auth = yield self._srp_auth(username, password) + returnValue(auth) @inlineCallbacks def _srp_auth(self, username, password): try: - extracted_username = self.extract_username(username) - auth = yield authenticate(self._leap_provider, extracted_username, password) + auth = yield self._bonafide_auth(username, password) except SRPAuthError: raise UnauthorizedLogin() + returnValue(auth) + + @inlineCallbacks + 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})) - def validate_username(self, username): + def clean_username(self, username): if '@' not in username: - return True + return username extracted_username = self.extract_username(username) - return self.username_with_domain(extracted_username) == username + if self.username_with_domain(extracted_username) == username: + return extracted_username + raise UnauthorizedLogin() def extract_username(self, username): return re.search('^([^@]+)@?.*$', username).group(1) def username_with_domain(self, username): return '%s@%s' % (username, self.domain) + + +class Authentication(object): + def __init__(self, username, token, uuid, session_id, user_attributes): + self.username = username + self.token = token + self.uuid = uuid + self.session_id = session_id + self._user_attributes = user_attributes + + def is_admin(self): + return self._user_attributes.get('is_admin', False) diff --git a/service/pixelated/config/leap.py b/service/pixelated/config/leap.py index b86b756e..e8814038 100644 --- a/service/pixelated/config/leap.py +++ b/service/pixelated/config/leap.py @@ -1,28 +1,21 @@ from __future__ import absolute_import -from collections import namedtuple -from twisted.cred.error import UnauthorizedLogin -from twisted.internet import defer, threads -from twisted.logger import Logger - -from leap.common.events import (server as events_server) -from leap.soledad.common.errors import InvalidAuthTokenError -from leap.bitmask.bonafide._srp import SRPAuthError -from leap.bitmask.bonafide.session import Session from leap.bitmask.bonafide.provider import Api - -from pixelated.config import credentials -from pixelated.config import leap_config +from leap.bitmask.bonafide.session import Session +from leap.common.events import (server as events_server) +from pixelated.adapter.welcome_mail import add_welcome_mail +from pixelated.authentication import Authenticator, Credentials, Authentication from pixelated.bitmask_libraries.certs import LeapCertificate from pixelated.bitmask_libraries.provider import LeapProvider +from pixelated.config import credentials +from pixelated.config import leap_config from pixelated.config.sessions import LeapSessionFactory +from twisted.internet import defer +from twisted.logger import Logger log = Logger() -Credentials = namedtuple('Credentials', 'username, password') - - def initialize_leap_provider(provider_hostname, provider_cert, provider_fingerprint, leap_home): LeapCertificate.set_cert_and_fingerprint(provider_cert, provider_fingerprint) @@ -64,10 +57,7 @@ def initialize_leap_single_user(leap_provider_cert, provider = initialize_leap_provider(provider, leap_provider_cert, leap_provider_cert_fingerprint, leap_home) - try: - auth = yield authenticate(provider, username, password) - except SRPAuthError: - raise UnauthorizedLogin() + auth = Authenticator(provider).authenticate(username, password) leap_session = yield create_leap_session(provider, username, password, auth) @@ -84,16 +74,29 @@ def authenticate(provider, user, password): def init_monkeypatches(): - import pixelated.extensions.requests_urllib3 + pass + + +class BootstrapUserServices(object): + + def __init__(self, services_factory, provider): + self._services_factory = services_factory + self._provider = provider + @defer.inlineCallbacks + def setup(self, user_auth, password, language='pt-BR'): + leap_session = yield create_leap_session(self._provider, user_auth.username, password, user_auth) + yield self._setup_user_services(leap_session) + yield self._add_welcome_email(leap_session, language) -class Authentication(object): - def __init__(self, username, token, uuid, session_id, user_attributes): - self.username = username - self.token = token - self.uuid = uuid - self.session_id = session_id - self._user_attributes = user_attributes + @defer.inlineCallbacks + def _setup_user_services(self, leap_session): + user_id = leap_session.user_auth.uuid + if not self._services_factory.has_session(user_id): + yield self._services_factory.create_services_from(leap_session) + self._services_factory.map_email(leap_session.user_auth.username, user_id) - def is_admin(self): - return self._user_attributes.get('is_admin', False) + @defer.inlineCallbacks + def _add_welcome_email(self, leap_session, language): + if leap_session.fresh_account: + yield add_welcome_mail(leap_session.mail_store, language) diff --git a/service/pixelated/config/services.py b/service/pixelated/config/services.py index 1f59c255..9e4de84e 100644 --- a/service/pixelated/config/services.py +++ b/service/pixelated/config/services.py @@ -39,7 +39,6 @@ class Services(object): self.keymanager = self._leap_session.keymanager self.draft_service = self._setup_draft_service(self._leap_session.mail_store) self.feedback_service = self._setup_feedback_service() - yield self._index_all_mails() def close(self): diff --git a/service/pixelated/register.py b/service/pixelated/register.py index 66ceea41..a91628e8 100644 --- a/service/pixelated/register.py +++ b/service/pixelated/register.py @@ -25,7 +25,7 @@ from pixelated.bitmask_libraries.certs import LeapCertificate from pixelated.bitmask_libraries.provider import LeapProvider from pixelated.config import arguments from pixelated.config import logger as logger_config -from pixelated.config.authentication import Authentication +from pixelated.config.leap import Authentication from pixelated.config.sessions import LeapSessionFactory from twisted.internet.defer import inlineCallbacks from twisted.logger import Logger diff --git a/service/pixelated/resources/login_resource.py b/service/pixelated/resources/login_resource.py index c0d9e874..d5555b90 100644 --- a/service/pixelated/resources/login_resource.py +++ b/service/pixelated/resources/login_resource.py @@ -17,20 +17,19 @@ import os from xml.sax import SAXParseException -from twisted.cred import credentials +from pixelated.authentication import Authenticator +from pixelated.config.leap import BootstrapUserServices +from pixelated.resources import BaseResource, UnAuthorizedResource, IPixelatedSession +from pixelated.resources import handle_error_deferred from twisted.internet import defer +from twisted.logger import Logger from twisted.python.filepath import FilePath from twisted.web import util from twisted.web.http import UNAUTHORIZED, OK -from twisted.web.resource import IResource, NoResource +from twisted.web.resource import NoResource from twisted.web.server import NOT_DONE_YET from twisted.web.static import File from twisted.web.template import Element, XMLFile, renderElement, renderer -from twisted.logger import Logger - -from pixelated.resources import handle_error_deferred -from pixelated.adapter.welcome_mail import add_welcome_mail -from pixelated.resources import BaseResource, UnAuthorizedResource, IPixelatedSession log = Logger() @@ -53,8 +52,8 @@ def _get_static_folder(): def parse_accept_language(all_headers): accepted_languages = ['pt-BR', 'en-US'] + languages = all_headers.get('accept-language', '').split(';')[0] for language in accepted_languages: - languages = all_headers['accept-language'].split(';')[0] if language in languages: return language return 'pt-BR' @@ -105,12 +104,15 @@ class LoginWebSite(Element): class LoginResource(BaseResource): BASE_URL = 'login' - def __init__(self, services_factory, portal=None, disclaimer_banner=None): + def __init__(self, services_factory, provider=None, disclaimer_banner=None, authenticator=None): BaseResource.__init__(self, services_factory) self._static_folder = _get_static_folder() self._startup_folder = _get_startup_folder() - self._portal = portal self._disclaimer_banner = disclaimer_banner + self._provider = provider + self._authenticator = authenticator or Authenticator(provider) + self._bootstrap_user_services = BootstrapUserServices(services_factory, provider) + self.putChild('startup-assets', File(self._startup_folder)) with open(os.path.join(self._startup_folder, 'Interstitial.html')) as f: self.interstitial = f.read() @@ -136,11 +138,11 @@ class LoginResource(BaseResource): if self.is_logged_in(request): return util.redirectTo("/", request) - def render_response(leap_session): + def render_response(user_auth): request.setResponseCode(OK) request.write(self.interstitial) request.finish() - self._setup_user_services(leap_session, request) + self._complete_bootstrap(user_auth, request) def render_error(error): log.info('Login Error for %s' % request.args['username'][0]) @@ -156,28 +158,21 @@ class LoginResource(BaseResource): @defer.inlineCallbacks def _handle_login(self, request): - self.creds = self._get_creds_from(request) - iface, leap_session, logout = yield self._portal.login(self.creds, None, IResource) - defer.returnValue(leap_session) - - def _get_creds_from(self, request): - username = request.args['username'][0].split('@')[0] + username = request.args['username'][0] password = request.args['password'][0] - return credentials.UsernamePassword(username, password) + user_auth = yield self._authenticator.authenticate(username, password) + defer.returnValue(user_auth) - @defer.inlineCallbacks - def _setup_user_services(self, leap_session, request): - user_id = leap_session.user_auth.uuid - if not self._services_factory.has_session(user_id): - yield self._services_factory.create_services_from(leap_session) - self._services_factory.map_email(self.creds.username, user_id) - - if leap_session.fresh_account: - language = parse_accept_language(request.getAllHeaders()) - yield add_welcome_mail(leap_session.mail_store, language) + def _complete_bootstrap(self, user_auth, request): + def log_error(error): + log.error('Login error during %s services setup: %s' % (user_auth.username, error.getErrorMessage())) - self._init_http_session(request, user_id) + def set_session_cookies(_): + session = IPixelatedSession(request.getSession()) + session.user_uuid = user_auth.uuid - def _init_http_session(self, request, user_id): - session = IPixelatedSession(request.getSession()) - session.user_uuid = user_id + language = parse_accept_language(request.getAllHeaders()) + password = request.args['password'][0] + d = self._bootstrap_user_services.setup(user_auth, password, language) + d.addCallback(set_session_cookies) + d.addErrback(log_error) diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py index 504d156d..70d8a565 100644 --- a/service/pixelated/resources/root_resource.py +++ b/service/pixelated/resources/root_resource.py @@ -19,6 +19,7 @@ import os from string import Template from pixelated.resources import BaseResource, UnAuthorizedResource, UnavailableResource +from pixelated.resources import IPixelatedSession from pixelated.resources.attachments_resource import AttachmentsResource from pixelated.resources.sandbox_resource import SandboxResource from pixelated.resources.contacts_resource import ContactsResource @@ -34,6 +35,10 @@ from pixelated.resources.keys_resource import KeysResource from twisted.web.resource import NoResource from twisted.web.static import File +from twisted.logger import Logger + +log = Logger() + from pixelated.resources.users import UsersResource CSRF_TOKEN_LENGTH = 32 @@ -82,13 +87,13 @@ class RootResource(BaseResource): csrf_input = request.args.get('csrftoken', [None])[0] or json.loads(request.content.read()).get('csrftoken', [None])[0] return csrf_input and csrf_input == xsrf_token - def initialize(self, portal=None, disclaimer_banner=None): + def initialize(self, provider=None, disclaimer_banner=None, authenticator=None): self._child_resources.add('sandbox', SandboxResource(self._static_folder)) self._child_resources.add('assets', File(self._static_folder)) self._child_resources.add('keys', KeysResource(self._services_factory)) self._child_resources.add(AttachmentsResource.BASE_URL, AttachmentsResource(self._services_factory)) self._child_resources.add('contacts', ContactsResource(self._services_factory)) - self._child_resources.add('features', FeaturesResource(portal)) + self._child_resources.add('features', FeaturesResource(provider)) self._child_resources.add('tags', TagsResource(self._services_factory)) self._child_resources.add('mails', MailsResource(self._services_factory)) self._child_resources.add('mail', MailResource(self._services_factory)) @@ -96,7 +101,7 @@ class RootResource(BaseResource): self._child_resources.add('user-settings', UserSettingsResource(self._services_factory)) self._child_resources.add('users', UsersResource(self._services_factory)) self._child_resources.add(LoginResource.BASE_URL, - LoginResource(self._services_factory, portal, disclaimer_banner=disclaimer_banner)) + LoginResource(self._services_factory, provider, disclaimer_banner=disclaimer_banner, authenticator=authenticator)) self._child_resources.add(LogoutResource.BASE_URL, LogoutResource(self._services_factory)) self._mode = MODE_RUNNING diff --git a/service/test/integration/test_logout.py b/service/test/integration/test_logout.py index e67fabab..c9d39d17 100644 --- a/service/test/integration/test_logout.py +++ b/service/test/integration/test_logout.py @@ -15,7 +15,6 @@ # along with Pixelated. If not, see <http://www.gnu.org/licenses/>. import json -from mock import patch from mockito import verify from twisted.internet import defer diff --git a/service/test/integration/test_multi_user_login.py b/service/test/integration/test_multi_user_login.py index c1500031..af2a81ac 100644 --- a/service/test/integration/test_multi_user_login.py +++ b/service/test/integration/test_multi_user_login.py @@ -35,6 +35,7 @@ class MultiUserLoginTest(MultiUserSoledadTestBase): def test_logged_in_users_sees_resources(self): response, login_request = yield self.app_test_client.login() yield response + mail = load_mail_from_file('mbox00000000') mail_id = yield self._create_mail_in_soledad(mail) expected_mail_dict = {'body': u'Dignissimos ducimus veritatis. Est tenetur consequatur quia occaecati. Vel sit sit voluptas.\n\nEarum distinctio eos. Accusantium qui sint ut quia assumenda. Facere dignissimos inventore autem sit amet. Pariatur voluptatem sint est.\n\nUt recusandae praesentium aspernatur. Exercitationem amet placeat deserunt quae consequatur eum. Unde doloremque suscipit quia.\n\n', 'header': {u'date': u'Tue, 21 Apr 2015 08:43:27 +0000 (UTC)', u'to': [u'carmel@murazikortiz.name'], u'x-tw-pixelated-tags': u'nite, macro, trash', u'from': u'darby.senger@zemlak.biz', u'subject': u'Itaque consequatur repellendus provident sunt quia.'}, 'ident': mail_id, 'status': [], 'tags': [], 'textPlainBody': u'Dignissimos ducimus veritatis. Est tenetur consequatur quia occaecati. Vel sit sit voluptas.\n\nEarum distinctio eos. Accusantium qui sint ut quia assumenda. Facere dignissimos inventore autem sit amet. Pariatur voluptatem sint est.\n\nUt recusandae praesentium aspernatur. Exercitationem amet placeat deserunt quae consequatur eum. Unde doloremque suscipit quia.\n\n', 'mailbox': u'inbox', 'attachments': [], 'security_casing': {'imprints': [{'state': 'no_signature_information'}], 'locks': []}} diff --git a/service/test/support/integration/app_test_client.py b/service/test/support/integration/app_test_client.py index f982407e..93dfd812 100644 --- a/service/test/support/integration/app_test_client.py +++ b/service/test/support/integration/app_test_client.py @@ -52,6 +52,7 @@ from pixelated.resources.root_resource import RootResource from test.support.integration.model import MailBuilder from test.support.test_helper import request_mock from test.support.integration.model import ResponseMail +from pixelated.config.sessions import SessionCache class AppTestAccount(object): @@ -143,6 +144,27 @@ class StubSRPChecker(object): return defer.fail() +class StubAuthenticator(object): + def __init__(self, provider, credentials={}): + self._leap_provider = provider + self._credentials = credentials.copy() + + def add_user(self, username, password): + self._credentials[username] = password + + def _set_leap_session_cache(self, auth): + key = SessionCache.session_key(self._leap_provider, 'username') + SessionCache.remember_session(key, LeapSession(self._leap_provider, auth, None, None, None, None)) + + def authenticate(self, username, password): + if self._credentials[username] == password: + leap_auth = Authentication(username, uuid.uuid4(), uuid.uuid4(), uuid.uuid4(), {}) + self._set_leap_session_cache(leap_auth) + return defer.succeed(leap_auth) + else: + return defer.fail() + + class StubServicesFactory(ServicesFactory): def __init__(self, accounts, mode): @@ -196,13 +218,16 @@ class AppTestClient(object): self.service_factory.add_session('someuserid', services) self.resource = RootResource(self.service_factory) - self.resource.initialize() + provider = mock() + self.resource.initialize(provider) else: self.service_factory = StubServicesFactory(self.accounts, mode) provider = mock() srp_checker = StubSRPChecker(provider) - srp_checker.add_user('username', 'password') - self.resource = set_up_protected_resources(RootResource(self.service_factory), provider, self.service_factory, checker=srp_checker) + bonafide_checker = StubAuthenticator(provider) + bonafide_checker.add_user('username', 'password') + + self.resource = set_up_protected_resources(RootResource(self.service_factory), provider, self.service_factory, checker=srp_checker, authenticator=bonafide_checker) @defer.inlineCallbacks def create_user(self, account_name): diff --git a/service/test/support/integration/multi_user_client.py b/service/test/support/integration/multi_user_client.py index 0257214f..28316d5b 100644 --- a/service/test/support/integration/multi_user_client.py +++ b/service/test/support/integration/multi_user_client.py @@ -13,8 +13,10 @@ # # 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 leap.bitmask.bonafide._srp import SRPAuthError from mock import patch from mockito import mock, when, any as ANY +from pixelated.authentication import Authenticator from pixelated.config.leap import Authentication from twisted.internet import defer @@ -49,9 +51,14 @@ class MultiUserClient(AppTestClient): self.credentials_checker = StubSRPChecker(leap_provider) self.resource = set_up_protected_resources(root_resource, leap_provider, self.service_factory, checker=self.credentials_checker) - def login(self, username='username', password='password'): - if(username == 'username' and password == 'password'): + def _mock_bonafide_auth(self, username, password): + if username == 'username' and password == 'password': self.credentials_checker.add_user(username, password) + when(Authenticator)._bonafide_auth(username, password).thenReturn(self.user_auth) + else: + when(Authenticator)._bonafide_auth(username, password).thenRaise(SRPAuthError) + + def login(self, username='username', password='password'): session = Authentication(username, 'some_user_token', 'some_user_uuid', 'session_id', {'is_admin': False}) leap_session = self._test_account.leap_session leap_session.user_auth = session @@ -63,6 +70,8 @@ class MultiUserClient(AppTestClient): self.services = self._test_account.services self.user_auth = session + self._mock_bonafide_auth(username, password) + when(LeapSessionFactory).create(username, password, session).thenReturn(leap_session) with patch('mockito.invocation.AnswerSelector', AnswerSelector): when(leap_session).initial_sync().thenAnswer(lambda: defer.succeed(None)) diff --git a/service/test/unit/config/test_leap.py b/service/test/unit/config/test_leap.py index ce533c7f..8f582dd8 100644 --- a/service/test/unit/config/test_leap.py +++ b/service/test/unit/config/test_leap.py @@ -1,8 +1,8 @@ from leap.soledad.common.errors import InvalidAuthTokenError -from mock import MagicMock, patch +from mock import MagicMock, patch, Mock from twisted.trial import unittest from twisted.internet import defer -from pixelated.config.leap import create_leap_session +from pixelated.config.leap import create_leap_session, BootstrapUserServices from pixelated.config.sessions import LeapSessionFactory, SessionCache @@ -35,3 +35,69 @@ class TestAuth(unittest.TestCase): self.assertFalse(session.first_required_sync.called) self.assertEqual(session, returned_session) + + +class TestUserBootstrap(unittest.TestCase): + + def setUp(self): + self._service_factory = Mock() + self._provider = Mock() + self._user_bootstrap = BootstrapUserServices(self._service_factory, self._provider) + + username = 'ayoyo' + password = 'ayoyo_password' + self.username = username + self.password = password + + user_auth = Mock() + user_auth.username = username + self.uuid = 'some_user_uuid' + user_auth.uuid = self.uuid + self.user_auth = user_auth + + leap_session = Mock() + leap_session.user_auth = user_auth + leap_session.fresh_account = False + self.leap_session = leap_session + + @patch('pixelated.config.leap.create_leap_session') + def test_should_create_leap_session(self, mock_create_leap_session): + mock_create_leap_session.return_value = self.leap_session + self._service_factory.has_session.return_value = False + + self._user_bootstrap.setup(self.user_auth, self.password) + + mock_create_leap_session.called_once_with(self._provider, self.username, self.password, self.user_auth) + + @patch('pixelated.config.leap.create_leap_session') + def test_should_setup_user_services_and_map_email(self, mock_create_leap_session): + mock_create_leap_session.return_value = self.leap_session + self._service_factory.has_session.return_value = False + + self._user_bootstrap.setup(self.user_auth, self.password) + + self._service_factory.create_services_from.assert_called_once_with(self.leap_session) + self._service_factory.map_email.assert_called_once_with(self.username, self.uuid) + + @patch('pixelated.config.leap.create_leap_session') + def test_should_not_user_services_if_there_is_already_a_session(self, mock_create_leap_session): + mock_create_leap_session.return_value = self.leap_session + self._service_factory.has_session.return_value = True + + self._user_bootstrap.setup(self.user_auth, self.password) + + self.assertFalse(self._service_factory.create_services_from.called) + + @patch('pixelated.config.leap.add_welcome_mail') + @patch('pixelated.config.leap.create_leap_session') + def test_should_add_welcome_email_on_a_fresh_account(self, mock_create_leap_session, mock_add_welcome_email): + self.leap_session.fresh_account = True + mail_store = Mock() + self.leap_session.mail_store = mail_store + mock_create_leap_session.return_value = self.leap_session + self._service_factory.has_session.return_value = False + some_language = 'en-US' + + self._user_bootstrap.setup(self.user_auth, self.password, '') + + mock_add_welcome_email.called_once_with(mail_store, some_language) diff --git a/service/test/unit/config/test_services.py b/service/test/unit/config/test_services.py index 8277c919..ed221261 100644 --- a/service/test/unit/config/test_services.py +++ b/service/test/unit/config/test_services.py @@ -15,6 +15,7 @@ # along with Pixelated. If not, see <http://www.gnu.org/licenses/>. import unittest +from mock import Mock, ANY, patch from mockito import mock, verify from pixelated.config.services import Services, ServicesFactory @@ -36,9 +37,58 @@ class ServicesTest(unittest.TestCase): class ServicesFactoryTest(unittest.TestCase): + def setUp(self): + self.service_factory = ServicesFactory(Mock()) + + def test_users_has_no_default_sessions(self): + user_id = ANY + self.assertFalse(self.service_factory.has_session(user_id)) + + def test_add_user_sessions(self): + user_id = 'irrelevant' + some_service = Mock() + + self.service_factory.add_session(user_id, some_service) + + self.assertTrue(self.service_factory.has_session(user_id)) + self.assertEqual(some_service, self.service_factory.services(user_id)) + def test_online_sessions_counts_logged_in_users(self): - service_factory = ServicesFactory(mock()) - service_factory.add_session('some_id1', mock()) - service_factory.add_session('some_id2', mock()) + self.service_factory.add_session('some_id1', mock()) + self.service_factory.add_session('some_id2', mock()) + + self.assertEqual(2, self.service_factory.online_sessions()) + + @patch('pixelated.config.services.Services.setup') + def test_create_services_from_leap_session_sets_up_services_and_add_a_user_session(self, mock_setup_services): + leap_session = Mock() + user_id = 'irrelevant' + leap_session.user_auth.uuid = user_id + + self.service_factory.create_services_from(leap_session) + + self.assertTrue(mock_setup_services.called) + self.assertTrue(self.service_factory.has_session(user_id)) + + def test_destroy_session_using_close_user_services_and_deletes_sessions(self): + user_id = 'irrelevant' + some_service = Mock() + self.service_factory.add_session(user_id, some_service) + + self.service_factory.destroy_session(user_id) + + self.assertFalse(self.service_factory.has_session(user_id)) + self.assertTrue(some_service.close.called) + + def test_sessions_can_be_destroyed_using_email_rather_than_uuid(self): + user_id = 'irrelevant' + username = 'haha' + email = '%s@ha.ha' % username + some_service = Mock() + self.service_factory.add_session(user_id, some_service) + self.service_factory.map_email(username, user_id) + + self.service_factory.destroy_session(email, using_email=True) - self.assertEqual(2, service_factory.online_sessions()) + self.assertFalse(self.service_factory.has_session(user_id)) + self.assertTrue(some_service.close.called) diff --git a/service/test/unit/resources/test_login_resource.py b/service/test/unit/resources/test_login_resource.py index 5843ef28..d3d7ba64 100644 --- a/service/test/unit/resources/test_login_resource.py +++ b/service/test/unit/resources/test_login_resource.py @@ -1,17 +1,15 @@ import os -from leap.bitmask.bonafide._srp import SRPAuthError from mock import patch -from mockito import mock, when, any as ANY, verify, verifyZeroInteractions, verifyNoMoreInteractions +from mockito import mock, when, any as ANY +from twisted.cred.error import UnauthorizedLogin +from twisted.internet import defer from twisted.trial import unittest -from twisted.web.resource import IResource from twisted.web.test.requesthelper import DummyRequest -from pixelated.config.sessions import LeapSession from pixelated.resources.login_resource import LoginResource from pixelated.resources.login_resource import parse_accept_language from test.unit.resources import DummySite -from test.support.mockito import AnswerSelector class TestParseAcceptLanguage(unittest.TestCase): @@ -163,9 +161,8 @@ class TestLoginResource(unittest.TestCase): class TestLoginPOST(unittest.TestCase): def setUp(self): self.services_factory = mock() - self.portal = mock() self.provider = mock() - self.resource = LoginResource(self.services_factory, self.portal) + self.resource = LoginResource(self.services_factory, self.provider) self.web = DummySite(self.resource) self.request = DummyRequest(['']) @@ -176,83 +173,85 @@ class TestLoginPOST(unittest.TestCase): self.password = password self.request.addArg('password', password) self.request.method = 'POST' - leap_session = mock(LeapSession) user_auth = mock() user_auth.uuid = 'some_user_uuid' - leap_session.user_auth = user_auth - config = mock() - config.leap_home = 'some_folder' - leap_session.config = config - leap_session.fresh_account = False - self.leap_session = leap_session self.user_auth = user_auth - def mock_user_has_services_setup(self): - when(self.services_factory).has_session('some_user_uuid').thenReturn(True) + @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): + mock_logged_in.return_value = True + when(self.services_factory).has_session(ANY()).thenReturn(True) + mock_redirect.return_value = "mocked redirection" - def test_login_responds_interstitial_and_add_corresponding_session_to_services_factory(self): - irrelevant = None - when(self.portal).login(ANY(), None, IResource).thenReturn((irrelevant, self.leap_session, irrelevant)) - with patch('mockito.invocation.AnswerSelector', AnswerSelector): - when(self.services_factory).create_services_from(self.leap_session).thenAnswer(self.mock_user_has_services_setup) + d = self.web.get(self.request) + + def assert_redirected_to_home(_): + mock_redirect.assert_called_once_with('/', self.request) + self.assertFalse(mock_authenticate.called) + + d.addCallback(assert_redirected_to_home) + return d + + @patch('pixelated.config.leap.BootstrapUserServices.setup') + @patch('pixelated.authentication.Authenticator.authenticate') + def test_should_return_form_back_with_error_message_when_login_fails(self, mock_authenticate, + mock_user_bootstrap_setup): + mock_authenticate.side_effect = UnauthorizedLogin() d = self.web.get(self.request) - def assert_login_setup_service_for_user(_): - verify(self.portal).login(ANY(), None, IResource) - verify(self.services_factory).create_services_from(self.leap_session) - verify(self.services_factory).map_email('ayoyo', 'some_user_uuid') - interstitial_js_in_template = '<script src="startup-assets/Interstitial.js"></script>' - self.assertIn(interstitial_js_in_template, self.request.written[0]) - self.assertTrue(self.resource.is_logged_in(self.request)) + def assert_error_response_and_user_services_not_setup(_): + mock_authenticate.assert_called_once_with(self.username, self.password) + self.assertEqual(401, self.request.responseCode) + written_response = ''.join(self.request.written) + self.assertIn('Invalid credentials', written_response) + self.assertFalse(mock_user_bootstrap_setup.called) + self.assertFalse(self.resource.get_session(self.request).is_logged_in()) - d.addCallback(assert_login_setup_service_for_user) + d.addCallback(assert_error_response_and_user_services_not_setup) return d - def test_login_does_not_reload_services_if_already_loaded(self): - irrelevant = None - when(self.portal).login(ANY(), None, IResource).thenReturn((irrelevant, self.leap_session, irrelevant)) - when(self.services_factory).has_session('some_user_uuid').thenReturn(True) + @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 d = self.web.get(self.request) - def assert_login_setup_service_for_user(_): - verify(self.portal).login(ANY(), None, IResource) - verify(self.services_factory).has_session('some_user_uuid') - verifyNoMoreInteractions(self.services_factory) + def assert_interstitial_in_response(_): + mock_authenticate.assert_called_once_with(self.username, self.password) interstitial_js_in_template = '<script src="startup-assets/Interstitial.js"></script>' self.assertIn(interstitial_js_in_template, self.request.written[0]) - self.assertTrue(self.resource.is_logged_in(self.request)) - d.addCallback(assert_login_setup_service_for_user) + d.addCallback(assert_interstitial_in_response) return d - def test_should_return_form_back_with_error_message_when_login_fails(self): - when(self.portal).login(ANY(), None, IResource).thenRaise(Exception()) + @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 + d = self.web.get(self.request) def assert_login_setup_service_for_user(_): - verify(self.portal).login(ANY(), None, IResource) - self.assertEqual(401, self.request.responseCode) - written_response = ''.join(self.request.written) - self.assertIn('Invalid credentials', written_response) - self.assertFalse(self.resource.is_logged_in(self.request)) + mock_user_bootstrap_setup.assert_called_once_with(self.user_auth, self.password, 'pt-BR') d.addCallback(assert_login_setup_service_for_user) return d - @patch('twisted.web.util.redirectTo') - @patch('pixelated.resources.session.PixelatedSession.is_logged_in') - def test_should_not_process_login_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" - when(self.portal).login(ANY(), None, IResource).thenRaise(Exception()) + @patch('pixelated.config.leap.BootstrapUserServices.setup') + @patch('pixelated.authentication.Authenticator.authenticate') + def test_successful_adds_cookies_to_indicat_logged_in_status_when_services_are_loaded(self, mock_authenticate, mock_user_bootstrap_setup): + mock_authenticate.return_value = self.user_auth + irrelevant = None + mock_user_bootstrap_setup.return_value = defer.succeed(irrelevant) + d = self.web.get(self.request) def assert_login_setup_service_for_user(_): - verifyZeroInteractions(self.portal) - mock_redirect.assert_called_once_with('/', self.request) + self.assertTrue(self.resource.get_session(self.request).is_logged_in()) d.addCallback(assert_login_setup_service_for_user) return d diff --git a/service/test/unit/test_authentication.py b/service/test/unit/test_authentication.py index cebb6543..b729e64f 100644 --- a/service/test/unit/test_authentication.py +++ b/service/test/unit/test_authentication.py @@ -8,7 +8,7 @@ from mock import patch, Mock from pixelated.authentication import Authenticator from pixelated.bitmask_libraries.provider import LeapProvider - +from pixelated.config.leap import Authentication PROVIDER_JSON = { "api_uri": "https://api.domain.org:4430", @@ -21,36 +21,60 @@ PROVIDER_JSON = { class AuthenticatorTest(unittest.TestCase): def setUp(self): + self._domain = 'domain.org' with patch.object(LeapProvider, 'fetch_provider_json', return_value=PROVIDER_JSON): - self._leap_provider = LeapProvider('domain.org') + self._leap_provider = LeapProvider(self._domain) @inlineCallbacks def test_bonafide_srp_exceptions_should_raise_unauthorized_login(self): auth = Authenticator(self._leap_provider) mock_bonafide_session = Mock() mock_bonafide_session.authenticate = Mock(side_effect=SRPAuthError()) - with patch('pixelated.config.leap.Session', return_value=mock_bonafide_session): + with patch('pixelated.authentication.Session', return_value=mock_bonafide_session): with self.assertRaises(UnauthorizedLogin): yield auth.authenticate('username', 'password') @inlineCallbacks - def test_auth_username_with_domain_only_makes_bonafide_auth_with_username(self): + def test_domain_name_is_stripped_before_making_bonafide_srp_auth(self): + username_without_domain = 'username' + username_with_domain = '%s@%s' % (username_without_domain, self._domain) auth = Authenticator(self._leap_provider) - with patch('pixelated.authentication.authenticate') as mock_leap_authenticate: - yield auth.authenticate('username@domain.org', 'password') - mock_leap_authenticate.assert_called_once_with(self._leap_provider, 'username', 'password') + with patch.object(Authenticator, '_bonafide_auth') as mock_leap_authenticate: + yield auth.authenticate(username_with_domain, 'password') + mock_leap_authenticate.assert_called_once_with(username_without_domain, 'password') + + @inlineCallbacks + def test_successful_bonafide_auth_should_return_the_user_authentication_object(self): + auth = Authenticator(self._leap_provider) + mock_bonafide_session = Mock() + mock_srp_auth = Mock() + mock_srp_auth.token = 'some_token' + mock_srp_auth.uuid = 'some_uuid' + mock_bonafide_session.authenticate = Mock(return_value=mock_srp_auth) + with patch('pixelated.authentication.Session', return_value=mock_srp_auth): + resulting_auth = yield auth.authenticate('username@domain.org', 'password') + self.assertIsInstance(resulting_auth, Authentication) + self.assertEquals('username', resulting_auth.username) + self.assertEquals('some_token', resulting_auth.token) + self.assertEquals('some_uuid', resulting_auth.uuid) - def test_validate_username_accepts_username(self): + def test_username_without_domain_is_not_changed(self): + username_without_domain = 'username' auth = Authenticator(self._leap_provider) - self.assertTrue(auth.validate_username('username')) + self.assertEqual(username_without_domain, auth.clean_username(username_without_domain)) - def test_validate_username_accepts_email_address(self): + def test_username_with_domain_is_stripped(self): + username_without_domain = 'username' + username_with_domain = '%s@%s' % (username_without_domain, self._domain) auth = Authenticator(self._leap_provider) - self.assertTrue(auth.validate_username('username@domain.org')) + self.assertEqual(username_without_domain, auth.clean_username(username_with_domain)) - def test_validate_username_denies_other_domains(self): + def test_username_with_wrong_domain_raises_exception(self): + username_without_domain = 'username' + username_with_wrong_domain = '%s@%s' % (username_without_domain, 'wrongdomain.org') auth = Authenticator(self._leap_provider) - self.assertFalse(auth.validate_username('username@wrongdomain.org')) + with self.assertRaises(UnauthorizedLogin): + auth.clean_username(username_with_wrong_domain) def test_username_with_domain(self): auth = Authenticator(self._leap_provider) |