summaryrefslogtreecommitdiff
path: root/service
diff options
context:
space:
mode:
authorJon Newson <jon_newson@ieee.org>2016-02-26 16:20:59 +1100
committerJon Newson <jon_newson@ieee.org>2016-02-26 16:20:59 +1100
commit05f4e2ca2d64eaba23c87df4d2e2cc9e09bba6de (patch)
tree50b2ccf6454f31a3f6bceaa997a5e2abbcb91a80 /service
parent52467b9aef76c9aac2f250478befd3afb7b6aabd (diff)
parentdbb434b56e6b161a3b851ae6a81f96dff14a29da (diff)
Merge branch 'master' of https://github.com/pixelated/pixelated-user-agent
# By Felix Hammerl (5) and others # Via NavaL * 'master' of https://github.com/pixelated/pixelated-user-agent: serving the client directly, as the current dependency on proxy strips out xsrf cookies -fixing functional test only adding feature resource in root_resource test -- fixing build changed logout to post Issue #612 Backend and frontend protection against csrf attacks: - root resources changes the csrf token cookie everytime it is loaded, in particular during the intestitial load during login - it will also add that cookie on single user mode - initialize will still load all resources - but they you cant access them if the csrf token do not match - all ajax calls needs to add the token to the header - non ajax get requests do not need xsrf token validation - non ajax post will have to send the token in as a form input or in the content Consolidate stylesheets Remove unused font and stylesheetgit s Create a new deferred for all IMAPAccount calls Clean up jshintrc Recreate session on soledad problems issue #617: Remove old html whitelister Issue #617: Sanitize received content
Diffstat (limited to 'service')
-rw-r--r--service/pixelated/bitmask_libraries/session.py24
-rw-r--r--service/pixelated/config/leap.py31
-rw-r--r--service/pixelated/resources/__init__.py4
-rw-r--r--service/pixelated/resources/auth.py1
-rw-r--r--service/pixelated/resources/login_resource.py3
-rw-r--r--service/pixelated/resources/logout_resource.py2
-rw-r--r--service/pixelated/resources/root_resource.py69
-rw-r--r--service/test/functional/features/environment.py6
-rw-r--r--service/test/integration/test_logout.py6
-rw-r--r--service/test/integration/test_retrieve_attachment.py17
-rw-r--r--service/test/support/integration/app_test_client.py23
-rw-r--r--service/test/support/integration/multi_user_client.py9
-rw-r--r--service/test/support/test_helper.py13
-rw-r--r--service/test/unit/bitmask_libraries/test_session.py12
-rw-r--r--service/test/unit/config/test_leap.py38
-rw-r--r--service/test/unit/resources/test_logout_resources.py8
-rw-r--r--service/test/unit/resources/test_root_resource.py83
17 files changed, 295 insertions, 54 deletions
diff --git a/service/pixelated/bitmask_libraries/session.py b/service/pixelated/bitmask_libraries/session.py
index f28d9f59..ae3eb992 100644
--- a/service/pixelated/bitmask_libraries/session.py
+++ b/service/pixelated/bitmask_libraries/session.py
@@ -53,18 +53,19 @@ class LeapSession(object):
self.fresh_account = False
self.incoming_mail_fetcher = None
self.account = None
- self._has_been_synced = False
+ self._has_been_initially_synced = False
self._sem_intial_sync = defer.DeferredLock()
+ self._is_closed = False
register(events.KEYMANAGER_FINISHED_KEY_GENERATION, self._set_fresh_account, uid=self.account_email())
@defer.inlineCallbacks
def initial_sync(self):
yield self._sem_intial_sync.acquire()
try:
- if not self._has_been_synced:
- yield self.sync()
+ yield self.sync()
+ if not self._has_been_initially_synced:
yield self.after_first_sync()
- self._has_been_synced = True
+ self._has_been_initially_synced = True
finally:
yield self._sem_intial_sync.release()
defer.returnValue(self)
@@ -81,8 +82,7 @@ class LeapSession(object):
reactor.callFromThread(self.incoming_mail_fetcher.startService)
def _create_account(self, user_mail, soledad):
- account = IMAPAccount(user_mail, soledad)
- return account
+ return IMAPAccount(user_mail, soledad, defer.Deferred())
def _set_fresh_account(self, event, email_address):
log.debug('Key for email %s has been generated' % email_address)
@@ -94,12 +94,17 @@ class LeapSession(object):
return self.provider.address_for(name)
def close(self):
+ self._is_closed = True
self.stop_background_jobs()
unregister(events.KEYMANAGER_FINISHED_KEY_GENERATION, uid=self.account_email())
self.soledad.close()
self.remove_from_cache()
self._close_account()
+ @property
+ def is_closed(self):
+ return self._is_closed
+
def _close_account(self):
if self.account:
self.account.end_session()
@@ -284,7 +289,12 @@ class SessionCache(object):
@staticmethod
def lookup_session(key):
- return SessionCache.sessions.get(key, None)
+ session = SessionCache.sessions.get(key, None)
+ if session is not None and session.is_closed:
+ SessionCache.remove_session(key)
+ return None
+ else:
+ return session
@staticmethod
def remember_session(key, session):
diff --git a/service/pixelated/config/leap.py b/service/pixelated/config/leap.py
index a8666086..17a69406 100644
--- a/service/pixelated/config/leap.py
+++ b/service/pixelated/config/leap.py
@@ -1,14 +1,13 @@
from __future__ import absolute_import
-from leap.common.events import (server as events_server,
- register, catalog as events)
+from leap.common.events import (server as events_server)
+from leap.soledad.common.errors import InvalidAuthTokenError
+
from pixelated.config import credentials
from pixelated.bitmask_libraries.config import LeapConfig
from pixelated.bitmask_libraries.certs import LeapCertificate
from pixelated.bitmask_libraries.provider import LeapProvider
from pixelated.bitmask_libraries.session import LeapSessionFactory
from twisted.internet import defer
-import os
-import logging
import logging
log = logging.getLogger(__name__)
@@ -39,11 +38,29 @@ def initialize_leap_multi_user(provider_hostname,
defer.returnValue((config, provider))
+def _create_session(provider, username, password, auth):
+ return LeapSessionFactory(provider).create(username, password, auth)
+
+
+def _force_close_session(session):
+ try:
+ session.close()
+ except Exception, e:
+ log.error(e)
+
+
@defer.inlineCallbacks
def authenticate_user(provider, username, password, initial_sync=True, auth=None):
- leap_session = LeapSessionFactory(provider).create(username, password, auth)
- if initial_sync:
- yield leap_session.initial_sync()
+ leap_session = _create_session(provider, username, password, auth)
+ try:
+ if initial_sync:
+ yield leap_session.initial_sync()
+ except InvalidAuthTokenError:
+ _force_close_session(leap_session)
+
+ leap_session = _create_session(provider, username, password, auth)
+ if initial_sync:
+ yield leap_session.initial_sync()
defer.returnValue(leap_session)
diff --git a/service/pixelated/resources/__init__.py b/service/pixelated/resources/__init__.py
index 14ecac86..469c8bc8 100644
--- a/service/pixelated/resources/__init__.py
+++ b/service/pixelated/resources/__init__.py
@@ -99,3 +99,7 @@ class UnAuthorizedResource(Resource):
def render_GET(self, request):
request.setResponseCode(UNAUTHORIZED)
return "Unauthorized!"
+
+ def render_POST(self, request):
+ request.setResponseCode(UNAUTHORIZED)
+ return "Unauthorized!"
diff --git a/service/pixelated/resources/auth.py b/service/pixelated/resources/auth.py
index 02729a01..a6ab5396 100644
--- a/service/pixelated/resources/auth.py
+++ b/service/pixelated/resources/auth.py
@@ -41,7 +41,6 @@ log = logging.getLogger(__name__)
class LeapPasswordChecker(object):
credentialInterfaces = (
credentials.IUsernamePassword,
- credentials.IUsernameHashedPassword
)
def __init__(self, leap_provider):
diff --git a/service/pixelated/resources/login_resource.py b/service/pixelated/resources/login_resource.py
index f1d9c1e3..aca266cf 100644
--- a/service/pixelated/resources/login_resource.py
+++ b/service/pixelated/resources/login_resource.py
@@ -20,14 +20,13 @@ from xml.sax import SAXParseException
from twisted.cred import credentials
from twisted.internet import defer
+from twisted.python.filepath import FilePath
from twisted.web import util
-from twisted.web.error import FlattenerError
from twisted.web.http import UNAUTHORIZED, OK
from twisted.web.resource import IResource, 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.python.filepath import FilePath
from pixelated.adapter.welcome_mail import add_welcome_mail
from pixelated.resources import BaseResource, UnAuthorizedResource, IPixelatedSession
diff --git a/service/pixelated/resources/logout_resource.py b/service/pixelated/resources/logout_resource.py
index fe80316e..344ad2e9 100644
--- a/service/pixelated/resources/logout_resource.py
+++ b/service/pixelated/resources/logout_resource.py
@@ -8,7 +8,7 @@ class LogoutResource(BaseResource):
BASE_URL = "logout"
isLeaf = True
- def render_GET(self, request):
+ def render_POST(self, request):
session = self.get_session(request)
self._services_factory.log_out_user(session.user_uuid)
session.expire()
diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py
index 6e619951..86435d89 100644
--- a/service/pixelated/resources/root_resource.py
+++ b/service/pixelated/resources/root_resource.py
@@ -13,11 +13,12 @@
#
# 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 hashlib
+import json
import os
from string import Template
-from pixelated.resources import BaseResource
+from pixelated.resources import BaseResource, UnAuthorizedResource
from pixelated.resources.attachments_resource import AttachmentsResource
from pixelated.resources.contacts_resource import ContactsResource
from pixelated.resources.features_resource import FeaturesResource
@@ -29,22 +30,22 @@ from pixelated.resources.mail_resource import MailResource
from pixelated.resources.mails_resource import MailsResource
from pixelated.resources.tags_resource import TagsResource
from pixelated.resources.keys_resource import KeysResource
-from twisted.web.resource import Resource
from twisted.web.static import File
+CSRF_TOKEN_LENGTH = 32
MODE_STARTUP = 1
MODE_RUNNING = 2
class RootResource(BaseResource):
-
def __init__(self, services_factory):
BaseResource.__init__(self, services_factory)
self._startup_assets_folder = self._get_startup_folder()
self._static_folder = self._get_static_folder()
self._html_template = open(os.path.join(self._static_folder, 'index.html')).read()
self._services_factory = services_factory
+ self._child_resources = ChildResourcesMap()
self._startup_mode()
def _startup_mode(self):
@@ -54,21 +55,39 @@ class RootResource(BaseResource):
def getChild(self, path, request):
if path == '':
return self
- return Resource.getChild(self, path, request)
+ if self._is_xsrf_valid(request):
+ return self._child_resources.get(path)
+ return UnAuthorizedResource()
+
+ def _is_xsrf_valid(self, request):
+ xsrf_token = request.getCookie('XSRF-TOKEN')
+
+ ajax_request = (request.getHeader('x-requested-with') == 'XMLHttpRequest')
+ if ajax_request:
+ xsrf_header = request.getHeader('x-xsrf-token')
+ return xsrf_header and xsrf_header == xsrf_token
+
+ get_request = (request.method == 'GET')
+ if get_request:
+ return True
+
+ 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):
- self.putChild('assets', File(self._static_folder))
- self.putChild('keys', KeysResource(self._services_factory))
- self.putChild(AttachmentsResource.BASE_URL, AttachmentsResource(self._services_factory))
- self.putChild('contacts', ContactsResource(self._services_factory))
- self.putChild('features', FeaturesResource(portal))
- self.putChild('tags', TagsResource(self._services_factory))
- self.putChild('mails', MailsResource(self._services_factory))
- self.putChild('mail', MailResource(self._services_factory))
- self.putChild('feedback', FeedbackResource(self._services_factory))
- self.putChild('user-settings', UserSettingsResource(self._services_factory))
- self.putChild(LoginResource.BASE_URL, LoginResource(self._services_factory, portal, disclaimer_banner=disclaimer_banner))
- self.putChild(LogoutResource.BASE_URL, LogoutResource(self._services_factory))
+ 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('tags', TagsResource(self._services_factory))
+ self._child_resources.add('mails', MailsResource(self._services_factory))
+ self._child_resources.add('mail', MailResource(self._services_factory))
+ self._child_resources.add('feedback', FeedbackResource(self._services_factory))
+ self._child_resources.add('user-settings', UserSettingsResource(self._services_factory))
+ self._child_resources.add(LoginResource.BASE_URL,
+ LoginResource(self._services_factory, portal, disclaimer_banner=disclaimer_banner))
+ self._child_resources.add(LogoutResource.BASE_URL, LogoutResource(self._services_factory))
self._mode = MODE_RUNNING
@@ -89,10 +108,26 @@ class RootResource(BaseResource):
def _is_starting(self):
return self._mode == MODE_STARTUP
+ def _add_csrf_cookie(self, request):
+ csrf_token = hashlib.sha256(os.urandom(CSRF_TOKEN_LENGTH)).hexdigest()
+ request.addCookie('XSRF-TOKEN', csrf_token)
+
def render_GET(self, request):
+ self._add_csrf_cookie(request)
if self._is_starting():
return open(os.path.join(self._startup_assets_folder, 'Interstitial.html')).read()
else:
account_email = self.mail_service(request).account_email
response = Template(self._html_template).safe_substitute(account_email=account_email)
return str(response)
+
+
+class ChildResourcesMap(object):
+ def __init__(self):
+ self._registry = {}
+
+ def add(self, path, resource):
+ self._registry[path] = resource
+
+ def get(self, path):
+ return self._registry.get(path)
diff --git a/service/test/functional/features/environment.py b/service/test/functional/features/environment.py
index 37b5d612..2c07faf3 100644
--- a/service/test/functional/features/environment.py
+++ b/service/test/functional/features/environment.py
@@ -44,11 +44,9 @@ def before_all(context):
PixelatedSite.disable_csp_requests()
client = AppTestClient()
start_app_test_client(client, UserAgentMode(is_single_user=True))
- client.listenTCP()
- proxy = Proxy(proxy_port='8889', app_port='4567')
+ client.listenTCP(port=8889)
FeaturesResource.DISABLED_FEATURES.append('autoRefresh')
context.client = client
- context.call_to_terminate_proxy = proxy.run_on_a_thread()
multi_user_client = AppTestClient()
start_app_test_client(multi_user_client, UserAgentMode(is_single_user=False))
@@ -57,7 +55,7 @@ def before_all(context):
def after_all(context):
- context.call_to_terminate_proxy()
+ context.client.stop()
def before_feature(context, feature):
diff --git a/service/test/integration/test_logout.py b/service/test/integration/test_logout.py
index 52f7e34f..da414126 100644
--- a/service/test/integration/test_logout.py
+++ b/service/test/integration/test_logout.py
@@ -13,10 +13,11 @@
#
# 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 json
+
from mockito import verify
from twisted.internet import defer
-from test.support.integration import load_mail_from_file
from test.support.integration.multi_user_client import MultiUserClient
from test.support.integration.soledad_test_base import SoledadTestBase
@@ -34,7 +35,8 @@ class MultiUserLogoutTest(MultiUserClient, SoledadTestBase):
yield self.wait_for_session_user_id_to_finish()
- response, request = self.get("/logout", as_json=False, from_request=login_request)
+ response, request = self.post("/logout", json.dumps({'csrftoken': [login_request.getCookie('XSRF-TOKEN')]}),
+ from_request=login_request, as_json=False)
yield response
self.assertEqual(302, request.responseCode) # redirected
diff --git a/service/test/integration/test_retrieve_attachment.py b/service/test/integration/test_retrieve_attachment.py
index 4aaeadc2..31c8c5df 100644
--- a/service/test/integration/test_retrieve_attachment.py
+++ b/service/test/integration/test_retrieve_attachment.py
@@ -43,6 +43,23 @@ class RetrieveAttachmentTest(SoledadTestBase):
self.assertEquals(expected_content_disposition, req.outgoingHeaders['content-disposition'])
self.assertEquals(expected_content_type, req.outgoingHeaders['content-type'])
+ @defer.inlineCallbacks
+ def test_should_retrieve_attachment_even_if_xsrf_token_not_passed(self):
+ attachment_id, input_mail = self._create_mail_with_attachment()
+ yield self.mail_store.add_mail('INBOX', input_mail.as_string())
+
+ requested_filename = "file name with space"
+ expected_content_type = 'text/plain'
+ expected_content_disposition = 'attachment; filename="file name with space"'
+
+ attachment, req = yield self.get_attachment(attachment_id, 'base64', filename=requested_filename,
+ content_type=expected_content_type, ajax=False, csrf='mismatched token')
+
+ self.assertEqual(200, req.code)
+ self.assertEquals('pretend to be binary attachment data', attachment)
+ self.assertEquals(expected_content_disposition, req.outgoingHeaders['content-disposition'])
+ self.assertEquals(expected_content_type, req.outgoingHeaders['content-type'])
+
def _create_mail_with_attachment(self):
input_mail = MIMEMultipart()
input_mail.attach(MIMEText(u'a utf8 message', _charset='utf-8'))
diff --git a/service/test/support/integration/app_test_client.py b/service/test/support/integration/app_test_client.py
index 8ab58397..f3ec5d25 100644
--- a/service/test/support/integration/app_test_client.py
+++ b/service/test/support/integration/app_test_client.py
@@ -244,22 +244,25 @@ class AppTestClient(object):
time.sleep(1)
return lambda: process.terminate()
- def get(self, path, get_args='', as_json=True):
- request = request_mock(path)
+ def stop(self):
+ reactor.stop()
+
+ def get(self, path, get_args='', as_json=True, ajax=True, csrf='token'):
+ request = request_mock(path, ajax=ajax, csrf=csrf)
request.args = get_args
return self._render(request, as_json)
- def post(self, path, body='', headers=None):
+ def post(self, path, body='', headers=None, ajax=True, csrf='token'):
headers = headers or {'Content-Type': 'application/json'}
- request = request_mock(path=path, method="POST", body=body, headers=headers)
+ request = request_mock(path=path, method="POST", body=body, headers=headers, ajax=ajax, csrf=csrf)
return self._render(request)
- def put(self, path, body):
- request = request_mock(path=path, method="PUT", body=body, headers={'Content-Type': ['application/json']})
+ def put(self, path, body, ajax=True, csrf='token'):
+ request = request_mock(path=path, method="PUT", body=body, headers={'Content-Type': ['application/json']}, ajax=ajax, csrf=csrf)
return self._render(request)
- def delete(self, path, body=""):
- request = request_mock(path=path, body=body, headers={'Content-Type': ['application/json']}, method="DELETE")
+ def delete(self, path, body="", ajax=True, csrf='token'):
+ request = request_mock(path=path, body=body, headers={'Content-Type': ['application/json']}, method="DELETE", ajax=ajax, csrf=csrf)
return self._render(request)
@defer.inlineCallbacks
@@ -322,13 +325,13 @@ class AppTestClient(object):
defer.returnValue(mails)
@defer.inlineCallbacks
- def get_attachment(self, ident, encoding, filename=None, content_type=None):
+ def get_attachment(self, ident, encoding, filename=None, content_type=None, ajax=True, csrf='token'):
params = {'encoding': [encoding]}
if filename:
params['filename'] = [filename]
if content_type:
params['content_type'] = [content_type]
- deferred_result, req = self.get("/attachment/%s" % ident, params, as_json=False)
+ deferred_result, req = self.get("/attachment/%s" % ident, params, as_json=False, ajax=ajax, csrf=csrf)
res = yield deferred_result
defer.returnValue((res, req))
diff --git a/service/test/support/integration/multi_user_client.py b/service/test/support/integration/multi_user_client.py
index fa65fb06..5f24456b 100644
--- a/service/test/support/integration/multi_user_client.py
+++ b/service/test/support/integration/multi_user_client.py
@@ -82,3 +82,12 @@ class MultiUserClient(AppTestClient):
session = from_request.getSession()
request.session = session
return self._render(request, as_json)
+
+ def post(self, path, body='', headers=None, ajax=True, csrf='token', as_json=True, from_request=None):
+ headers = headers or {'Content-Type': 'application/json'}
+ request = request_mock(path=path, method="POST", body=body, headers=headers, ajax=ajax, csrf=csrf)
+
+ if from_request:
+ session = from_request.getSession()
+ request.session = session
+ return self._render(request, as_json)
diff --git a/service/test/support/test_helper.py b/service/test/support/test_helper.py
index 77c74407..640baf6f 100644
--- a/service/test/support/test_helper.py
+++ b/service/test/support/test_helper.py
@@ -88,6 +88,7 @@ class PixRequestMock(DummyRequest):
DummyRequest.__init__(self, path)
self.content = None
self.code = None
+ self.cookies = {}
def getWrittenData(self):
if len(self.written):
@@ -97,8 +98,14 @@ class PixRequestMock(DummyRequest):
self.setResponseCode(302)
self.setHeader(b"location", url)
+ def addCookie(self, key, value):
+ self.cookies[key] = value
-def request_mock(path='', method='GET', body='', headers={}):
+ def getCookie(self, key):
+ return self.cookies.get(key)
+
+
+def request_mock(path='', method='GET', body='', headers={}, ajax=True, csrf='token'):
dummy = PixRequestMock(path.split('/'))
for name, val in headers.iteritems():
dummy.headers[name.lower()] = val
@@ -108,5 +115,9 @@ def request_mock(path='', method='GET', body='', headers={}):
else:
for key, val in body.items():
dummy.addArg(key, val)
+ if ajax:
+ dummy.headers['x-requested-with'] = 'XMLHttpRequest'
+ dummy.headers['x-xsrf-token'] = csrf
+ dummy.addCookie('XSRF-TOKEN', csrf)
return dummy
diff --git a/service/test/unit/bitmask_libraries/test_session.py b/service/test/unit/bitmask_libraries/test_session.py
index a41cb805..aad2cac2 100644
--- a/service/test/unit/bitmask_libraries/test_session.py
+++ b/service/test/unit/bitmask_libraries/test_session.py
@@ -133,6 +133,16 @@ class SessionTest(AbstractLeapTest):
self.assertTrue(session.fresh_account)
@patch('pixelated.bitmask_libraries.session.register')
+ def test_closed_session_not_reused(self, _):
+ session = self._create_session()
+ SessionCache.remember_session('somekey', session)
+ session._is_closed = True
+
+ result = SessionCache.lookup_session('somekey')
+
+ self.assertIsNone(result)
+
+ @patch('pixelated.bitmask_libraries.session.register')
def test_session_does_not_set_status_fresh_for_unkown_emails(self, _):
session = self._create_session()
self.provider.address_for.return_value = 'someone@somedomain.tld'
@@ -150,7 +160,7 @@ class SessionTest(AbstractLeapTest):
with patch('pixelated.bitmask_libraries.session.reactor.callFromThread', new=_execute_func) as _:
with patch.object(LeapSession, '_create_incoming_mail_fetcher', return_value=mailFetcherMock) as _:
session = self._create_session()
- session._has_been_synced = True
+ session._has_been_initially_synced = True
yield session.initial_sync()
self.assertFalse(mailFetcherMock.startService.called)
diff --git a/service/test/unit/config/test_leap.py b/service/test/unit/config/test_leap.py
new file mode 100644
index 00000000..6b34d717
--- /dev/null
+++ b/service/test/unit/config/test_leap.py
@@ -0,0 +1,38 @@
+from leap.soledad.common.errors import InvalidAuthTokenError
+from mock import MagicMock, patch
+from twisted.trial import unittest
+from twisted.internet import defer
+from pixelated.config.leap import authenticate_user
+
+
+class TestAuth(unittest.TestCase):
+
+ @patch('pixelated.config.leap.LeapSessionFactory')
+ @defer.inlineCallbacks
+ def test_authenticate_user_calls_initinal_sync(self, session_factory__ctor_mock):
+ session_factory_mock = session_factory__ctor_mock.return_value
+ provider_mock = MagicMock()
+ auth_mock = MagicMock()
+ session = MagicMock()
+
+ session_factory_mock.create.return_value = session
+
+ yield authenticate_user(provider_mock, 'username', 'password', auth=auth_mock)
+
+ session.initial_sync.assert_called_with()
+
+ @patch('pixelated.config.leap.LeapSessionFactory')
+ @defer.inlineCallbacks
+ def test_authenticate_user_calls_initial_sync_a_second_time_if_invalid_auth_exception_is_raised(self, session_factory__ctor_mock):
+ session_factory_mock = session_factory__ctor_mock.return_value
+ provider_mock = MagicMock()
+ auth_mock = MagicMock()
+ session = MagicMock()
+
+ session.initial_sync.side_effect = [InvalidAuthTokenError, defer.succeed(None)]
+ session_factory_mock.create.return_value = session
+
+ yield authenticate_user(provider_mock, 'username', 'password', auth=auth_mock)
+
+ session.close.assert_called_with()
+ self.assertEqual(2, session.initial_sync.call_count)
diff --git a/service/test/unit/resources/test_logout_resources.py b/service/test/unit/resources/test_logout_resources.py
index 48cf9db9..6246eeb9 100644
--- a/service/test/unit/resources/test_logout_resources.py
+++ b/service/test/unit/resources/test_logout_resources.py
@@ -1,6 +1,7 @@
from mock import patch
from mockito import mock, verify
from twisted.trial import unittest
+from twisted.web.error import UnsupportedMethod
from twisted.web.test.requesthelper import DummyRequest
from pixelated.resources.logout_resource import LogoutResource
@@ -16,6 +17,7 @@ class TestLogoutResource(unittest.TestCase):
@patch('twisted.web.util.redirectTo')
def test_logout(self, mock_redirect):
request = DummyRequest(['/logout'])
+ request.method = 'POST'
mock_redirect.return_value = 'haha'
@@ -29,3 +31,9 @@ class TestLogoutResource(unittest.TestCase):
d.addCallback(expire_session_and_redirect)
return d
+
+ def test_get_is_not_supported_for_logout(self):
+ request = DummyRequest(['/logout'])
+ request.method = 'GET'
+
+ self.assertRaises(UnsupportedMethod, self.web.get, request)
diff --git a/service/test/unit/resources/test_root_resource.py b/service/test/unit/resources/test_root_resource.py
index 3b0846ee..cc052d8b 100644
--- a/service/test/unit/resources/test_root_resource.py
+++ b/service/test/unit/resources/test_root_resource.py
@@ -1,11 +1,14 @@
import unittest
import re
+
+from mock import MagicMock, patch
from mockito import mock, when, any as ANY
from pixelated.application import UserAgentMode
+from pixelated.resources.features_resource import FeaturesResource
from test.unit.resources import DummySite
from twisted.web.test.requesthelper import DummyRequest
-from pixelated.resources.root_resource import RootResource
+from pixelated.resources.root_resource import RootResource, MODE_STARTUP, MODE_RUNNING
class TestRootResource(unittest.TestCase):
@@ -25,9 +28,11 @@ class TestRootResource(unittest.TestCase):
root_resource._html_template = "<html><head><title>$account_email</title></head></html>"
root_resource._mode = root_resource
self.web = DummySite(root_resource)
+ self.root_resource = root_resource
def test_render_GET_should_template_account_email(self):
request = DummyRequest([''])
+ request.addCookie = lambda key, value: 'stubbed'
d = self.web.get(request)
@@ -38,3 +43,79 @@ class TestRootResource(unittest.TestCase):
d.addCallback(assert_response)
return d
+
+ def _test_should_renew_xsrf_cookie(self):
+ request = DummyRequest([''])
+ request.addCookie = MagicMock()
+ generated_csrf_token = 'csrf_token'
+ mock_sha = MagicMock()
+ mock_sha.hexdigest = MagicMock(return_value=generated_csrf_token)
+
+ with patch('hashlib.sha256', return_value=mock_sha):
+ d = self.web.get(request)
+
+ def assert_csrf_cookie(_):
+ request.addCookie.assert_called_once_with('XSRF-TOKEN', generated_csrf_token)
+
+ d.addCallback(assert_csrf_cookie)
+ return d
+
+ def test_should_renew_xsrf_cookie_on_startup_mode(self):
+ self.root_resource._mode = MODE_STARTUP
+ self._test_should_renew_xsrf_cookie()
+
+ def test_should_renew_xsrf_cookie_on_running_mode(self):
+ self.root_resource._mode = MODE_RUNNING
+ self._test_should_renew_xsrf_cookie()
+
+ def _mock_ajax_csrf(self, request, csrf_token):
+ request.headers['x-requested-with'] = 'XMLHttpRequest'
+ request.headers['x-xsrf-token'] = csrf_token
+
+ def test_should_unauthorize_child_resource_ajax_requests_when_csrf_mismatch(self):
+ request = DummyRequest(['/child'])
+ self._mock_ajax_csrf(request, 'stubbed csrf token')
+
+ request.getCookie = MagicMock(return_value='mismatched csrf token')
+
+ d = self.web.get(request)
+
+ def assert_unauthorized(_):
+ self.assertEqual(401, request.responseCode)
+ self.assertEqual("Unauthorized!", request.written[0])
+
+ d.addCallback(assert_unauthorized)
+ return d
+
+ def test_should_authorize_child_resource_non_ajax_GET_requests(self):
+ request = DummyRequest(['features'])
+
+ request.getCookie = MagicMock(return_value='irrelevant -- stubbed')
+ self.root_resource._child_resources.add('features', FeaturesResource())
+
+ d = self.web.get(request)
+
+ def assert_unauthorized(_):
+ self.assertEqual(200, request.code)
+
+ d.addCallback(assert_unauthorized)
+ return d
+
+ def test_should_unauthorize_child_resource_non_ajax_POST_requests_when_csrf_input_mismatch(self):
+ request = DummyRequest(['mails'])
+ request.method = 'POST'
+ request.addArg('csrftoken', 'some csrf token')
+ mock_content = MagicMock()
+ mock_content.read = MagicMock(return_value={})
+ request.content = mock_content
+
+ request.getCookie = MagicMock(return_value='mismatched csrf token')
+
+ d = self.web.get(request)
+
+ def assert_unauthorized(_):
+ self.assertEqual(401, request.responseCode)
+ self.assertEqual("Unauthorized!", request.written[0])
+
+ d.addCallback(assert_unauthorized)
+ return d