diff options
Diffstat (limited to 'service')
-rw-r--r-- | service/pixelated/application.py | 2 | ||||
-rw-r--r-- | service/pixelated/bitmask_libraries/provider.py | 2 | ||||
-rw-r--r-- | service/pixelated/resources/__init__.py | 10 | ||||
-rw-r--r-- | service/pixelated/resources/auth.py | 31 | ||||
-rw-r--r-- | service/pixelated/resources/login_resource.py | 13 | ||||
-rw-r--r-- | service/pixelated/resources/root_resource.py | 95 | ||||
-rw-r--r-- | service/test/integration/test_feedback_service.py | 2 | ||||
-rw-r--r-- | service/test/unit/resources/test_auth.py | 85 | ||||
-rw-r--r-- | service/test/unit/resources/test_root_resource.py | 95 |
9 files changed, 256 insertions, 79 deletions
diff --git a/service/pixelated/application.py b/service/pixelated/application.py index 46e5ba85..fa6568e6 100644 --- a/service/pixelated/application.py +++ b/service/pixelated/application.py @@ -159,7 +159,7 @@ def set_up_protected_resources(root_resource, provider, services_factory, banner _portal = portal.Portal(realm, [session_checker, AllowAnonymousAccess()]) anonymous_resource = LoginResource(services_factory, provider, disclaimer_banner=banner, authenticator=authenticator) - protected_resource = PixelatedAuthSessionWrapper(_portal, root_resource, anonymous_resource, []) + protected_resource = PixelatedAuthSessionWrapper(_portal, root_resource, anonymous_resource) root_resource.initialize(provider, disclaimer_banner=banner, authenticator=authenticator) return protected_resource diff --git a/service/pixelated/bitmask_libraries/provider.py b/service/pixelated/bitmask_libraries/provider.py index 96935fbc..bc19f79e 100644 --- a/service/pixelated/bitmask_libraries/provider.py +++ b/service/pixelated/bitmask_libraries/provider.py @@ -193,7 +193,7 @@ class LeapProvider(object): fin.close() def setup_ca_bundle(self): - path = os.path.join(leap_config.leap_home, 'providers', self.server_name, 'keys', 'client') + path = os.path.dirname(self.provider_api_cert) if not os.path.isdir(path): os.makedirs(path, 0700) self._download_cert(self.provider_api_cert) diff --git a/service/pixelated/resources/__init__.py b/service/pixelated/resources/__init__.py index 11611f0b..97346a6f 100644 --- a/service/pixelated/resources/__init__.py +++ b/service/pixelated/resources/__init__.py @@ -13,8 +13,9 @@ # # 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 twisted.web.http import UNAUTHORIZED from twisted.web.resource import Resource @@ -26,6 +27,8 @@ from twisted.web.http import INTERNAL_SERVER_ERROR, SERVICE_UNAVAILABLE log = Logger() +CSRF_TOKEN_LENGTH = 32 + class SetEncoder(json.JSONEncoder): def default(self, obj): @@ -62,6 +65,11 @@ class BaseResource(Resource): Resource.__init__(self) self._services_factory = services_factory + def _add_csrf_cookie(self, request): + csrf_token = hashlib.sha256(os.urandom(CSRF_TOKEN_LENGTH)).hexdigest() + request.addCookie('XSRF-TOKEN', csrf_token) + log.debug('XSRF-TOKEN added: %s' % csrf_token) + def _get_user_id_from_request(self, request): if self._services_factory.mode.is_single_user: return None # it doesn't matter diff --git a/service/pixelated/resources/auth.py b/service/pixelated/resources/auth.py index adac985f..a2054f18 100644 --- a/service/pixelated/resources/auth.py +++ b/service/pixelated/resources/auth.py @@ -64,10 +64,18 @@ class SessionChecker(object): class PixelatedRealm(object): implements(portal.IRealm) + def __init__(self, authenticated_resource, public_resource): + self._authenticated_resource = authenticated_resource + self._public_resource = public_resource + def requestAvatar(self, avatarId, mind, *interfaces): - if IResource in interfaces: - return IResource, avatarId, lambda: None - raise NotImplementedError() + if IResource not in interfaces: + raise NotImplementedError() + if avatarId == checkers.ANONYMOUS: + avatar = self._public_resource + else: + avatar = self._authenticated_resource + return IResource, avatar, lambda: None @implementer(IResource) @@ -75,7 +83,7 @@ class PixelatedAuthSessionWrapper(object): isLeaf = False - def __init__(self, portal, root_resource, anonymous_resource, credentialFactories): + def __init__(self, portal, root_resource, anonymous_resource, credentialFactories=[]): self._portal = portal self._credentialFactories = credentialFactories self._root_resource = root_resource @@ -93,23 +101,18 @@ class PixelatedAuthSessionWrapper(object): return util.DeferredResource(self._login(creds, request)) def _login(self, credentials, request): - pattern = re.compile("^/sandbox/") - def loginSucceeded(args): interface, avatar, logout = args - if avatar == checkers.ANONYMOUS and not pattern.match(request.path): - return self._anonymous_resource - else: - return self._root_resource + # TODO: make sandbox public + return avatar def loginFailed(result): if result.check(error.Unauthorized, error.LoginFailed): return UnauthorizedResource(self._credentialFactories) else: - log.err( - result, - "HTTPAuthSessionWrapper.getChildWithDefault encountered " - "unexpected error") + log.error( + "PixelatedAuthSessionWrapper.getChildWithDefault encountered " + "unexpected error: %s" % result) return ErrorPage(500, None, None) d = self._portal.login(credentials, None, IResource) diff --git a/service/pixelated/resources/login_resource.py b/service/pixelated/resources/login_resource.py index aadc435e..fec4307e 100644 --- a/service/pixelated/resources/login_resource.py +++ b/service/pixelated/resources/login_resource.py @@ -39,6 +39,17 @@ def _get_startup_folder(): return os.path.join(path, '..', 'assets') +def _get_public_folder(): + static_folder = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "web-ui", "public")) + # this is a workaround for packaging + if not os.path.exists(static_folder): + static_folder = os.path.abspath( + os.path.join(os.path.abspath(__file__), "..", "..", "..", "..", "web-ui", "public")) + if not os.path.exists(static_folder): + static_folder = os.path.join('/', 'usr', 'share', 'pixelated-user-agent') + return static_folder + + def _get_static_folder(): static_folder = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "web-ui", "app")) # this is a workaround for packaging @@ -107,6 +118,7 @@ class LoginResource(BaseResource): def __init__(self, services_factory, provider=None, disclaimer_banner=None, authenticator=None): BaseResource.__init__(self, services_factory) self._static_folder = _get_static_folder() + self._public_folder = _get_public_folder() self._startup_folder = _get_startup_folder() self._disclaimer_banner = disclaimer_banner self._provider = provider @@ -114,6 +126,7 @@ class LoginResource(BaseResource): self._bootstrap_user_services = BootstrapUserServices(services_factory, provider) self.putChild('startup-assets', File(self._startup_folder)) + self.putChild('public-assets', File(self._public_folder)) with open(os.path.join(self._startup_folder, 'Interstitial.html')) as f: self.interstitial = f.read() diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py index 8df76c70..0788ffb1 100644 --- a/service/pixelated/resources/root_resource.py +++ b/service/pixelated/resources/root_resource.py @@ -13,12 +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 hashlib import json import os -from string import Template from pixelated.resources.users import UsersResource +import pixelated from pixelated.resources import BaseResource, UnAuthorizedResource, UnavailableResource from pixelated.resources import IPixelatedSession from pixelated.resources.attachments_resource import AttachmentsResource @@ -33,43 +32,48 @@ 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 pixelated.resources.inbox_resource import InboxResource, MODE_STARTUP, MODE_RUNNING from twisted.web.resource import NoResource from twisted.web.static import File from twisted.logger import Logger -log = Logger() +logger = Logger() -CSRF_TOKEN_LENGTH = 32 +class PublicRootResource(BaseResource): -MODE_STARTUP = 1 -MODE_RUNNING = 2 + def __init__(self, services_factory): + BaseResource.__init__(self, services_factory) -class RootResource(BaseResource): +class RootResource(PublicRootResource): + def __init__(self, services_factory): - BaseResource.__init__(self, services_factory) + PublicRootResource.__init__(self, services_factory) + self._assets_folder = self._get_assets_folder() 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() with open(os.path.join(self._startup_assets_folder, 'Interstitial.html')) as f: self.interstitial = f.read() + self._inbox_resource = InboxResource(services_factory) self._startup_mode() def _startup_mode(self): + self.putChild('assets', File(self._assets_folder)) self.putChild('startup-assets', File(self._startup_assets_folder)) self._mode = MODE_STARTUP + logger.debug('Root in STARTUP mode. %s' % self) - def getChild(self, path, request): + def getChildWithDefault(self, path, request): if path == '': - return self + return self._inbox_resource if self._mode == MODE_STARTUP: return UnavailableResource() if self._is_xsrf_valid(request): - return self._child_resources.get(path) + return BaseResource.getChildWithDefault(self, path, request) return UnAuthorizedResource() def _is_xsrf_valid(self, request): @@ -78,7 +82,9 @@ class RootResource(BaseResource): return True xsrf_token = request.getCookie('XSRF-TOKEN') + logger.debug('CSRF token: %s' % xsrf_token) + # TODO: how is comparing the cookie-csrf with the HTTP-header-csrf adding any csrf protection? ajax_request = (request.getHeader('x-requested-with') == 'XMLHttpRequest') if ajax_request: xsrf_header = request.getHeader('x-xsrf-token') @@ -88,24 +94,30 @@ class RootResource(BaseResource): return csrf_input and csrf_input == xsrf_token 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(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)) - self._child_resources.add('feedback', FeedbackResource(self._services_factory)) - 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, provider, disclaimer_banner=disclaimer_banner, authenticator=authenticator)) - self._child_resources.add(LogoutResource.BASE_URL, LogoutResource(self._services_factory)) - + self.putChild('sandbox', SandboxResource(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(provider)) + 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('users', UsersResource(self._services_factory)) + self.putChild(LoginResource.BASE_URL, + LoginResource(self._services_factory, provider, disclaimer_banner=disclaimer_banner, authenticator=authenticator)) + self.putChild(LogoutResource.BASE_URL, LogoutResource(self._services_factory)) + + self._inbox_resource.initialize() self._mode = MODE_RUNNING + logger.debug('Root in RUNNING mode. %s' % self) + + def _get_assets_folder(self): + pixelated_path = os.path.dirname(os.path.abspath(pixelated.__file__)) + return os.path.join(pixelated_path, '..', '..', 'web-ui', 'public') + # TODO: use the public folder for this def _get_startup_folder(self): path = os.path.dirname(os.path.abspath(__file__)) return os.path.join(path, '..', 'assets') @@ -119,30 +131,3 @@ class RootResource(BaseResource): if not os.path.exists(static_folder): static_folder = os.path.join('/', 'usr', 'share', 'pixelated-user-agent') return static_folder - - 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 self.interstitial - 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) or NoResource() diff --git a/service/test/integration/test_feedback_service.py b/service/test/integration/test_feedback_service.py index c50c1883..ff659396 100644 --- a/service/test/integration/test_feedback_service.py +++ b/service/test/integration/test_feedback_service.py @@ -1,4 +1,4 @@ -import unittest +from twisted.trial import unittest from httmock import urlmatch, HTTMock from mockito import when from twisted.internet import defer diff --git a/service/test/unit/resources/test_auth.py b/service/test/unit/resources/test_auth.py new file mode 100644 index 00000000..6bd0338a --- /dev/null +++ b/service/test/unit/resources/test_auth.py @@ -0,0 +1,85 @@ +from mockito import mock, when, any as ANY +from pixelated.resources.auth import SessionChecker, PixelatedRealm, PixelatedAuthSessionWrapper +from pixelated.resources.login_resource import LoginResource +from pixelated.resources.root_resource import RootResource +from test.unit.resources import DummySite +from twisted.cred import error +from twisted.cred.checkers import ANONYMOUS, AllowAnonymousAccess +from twisted.cred.portal import Portal +from twisted.internet.defer import succeed, fail +from twisted.python import failure +from twisted.trial import unittest +from twisted.web._auth.wrapper import UnauthorizedResource +from twisted.web.resource import IResource, getChildForRequest +from twisted.web.test.requesthelper import DummyRequest + + +class TestPixelatedRealm(unittest.TestCase): + + def setUp(self): + self.authenticated_root_resource = mock() + self.public_root_resource = mock() + self.realm = PixelatedRealm(self.authenticated_root_resource, self.public_root_resource) + + def test_anonymous_user_gets_anonymous_resource(self): + interface, avatar, logout_handler = self.realm.requestAvatar(ANONYMOUS, None, IResource) + self.assertEqual(interface, IResource) + self.assertIs(avatar, self.public_root_resource) + + def test_authenticated_user_gets_root_resource(self): + interface, avatar, logout_handler = self.realm.requestAvatar('username', None, IResource) + self.assertEqual(interface, IResource) + self.assertIs(avatar, self.authenticated_root_resource) + + +class TestPixelatedAuthSessionWrapper(unittest.TestCase): + + def setUp(self): + self.realm_mock = mock() + services_factory = mock() + session_checker = SessionChecker(services_factory) + self.portal = Portal(self.realm_mock, [session_checker, AllowAnonymousAccess()]) + self.user_uuid_mock = mock() + self.root_resource = RootResource(services_factory) + self.anonymous_resource_mock = mock() + + self.session_wrapper = PixelatedAuthSessionWrapper(self.portal, self.root_resource, self.anonymous_resource_mock) + self.request = DummyRequest([]) + self.request.prepath = [''] + self.request.path = '/' + + def test_should_proxy_to_login_resource_when_the_user_is_not_logged_in(self): + when(self.realm_mock).requestAvatar(ANONYMOUS, None, IResource).thenReturn((IResource, self.anonymous_resource_mock, lambda: None)) + + deferred_resource = self.session_wrapper.getChildWithDefault('', self.request) + d = deferred_resource.d + + def assert_anonymous_resource(resource): + self.assertIs(resource, self.anonymous_resource_mock) + + d.addCallback(assert_anonymous_resource) + return d + + def test_should_proxy_to_root_resource_when_the_user_is_logged_in(self): + when(self.realm_mock).requestAvatar(ANY(), None, IResource).thenReturn((IResource, self.root_resource, lambda: None)) + + deferred_resource = self.session_wrapper.getChildWithDefault('', self.request) + d = deferred_resource.d + + def assert_root_resource(resource): + self.assertIs(resource, self.root_resource) + + d.addCallback(assert_root_resource) + return d + + def test_should_X_when_unauthenticated_user_requests_non_public_resource(self): + when(self.realm_mock).requestAvatar(ANONYMOUS, None, IResource).thenReturn((IResource, self.anonymous_resource_mock, lambda: None)) + + deferred_resource = self.session_wrapper.getChildWithDefault('', self.request) + d = deferred_resource.d + + def assert_unauthorized_resource(resource): + self.assertIs(resource, self.anonymous_resource_mock) + + d.addCallback(assert_unauthorized_resource) + return d diff --git a/service/test/unit/resources/test_root_resource.py b/service/test/unit/resources/test_root_resource.py index 4ff11ce8..2c74d7b9 100644 --- a/service/test/unit/resources/test_root_resource.py +++ b/service/test/unit/resources/test_root_resource.py @@ -1,14 +1,20 @@ -import unittest +import os import re from mock import MagicMock, patch from mockito import mock, when, any as ANY +import pixelated from pixelated.application import UserAgentMode from pixelated.resources.features_resource import FeaturesResource from test.unit.resources import DummySite +from twisted.cred.checkers import ANONYMOUS +from twisted.internet.defer import succeed +from twisted.trial import unittest +from twisted.web.resource import IResource +from twisted.web.static import File from twisted.web.test.requesthelper import DummyRequest -from pixelated.resources.root_resource import RootResource, MODE_STARTUP, MODE_RUNNING +from pixelated.resources.root_resource import InboxResource, RootResource, MODE_STARTUP, MODE_RUNNING class TestRootResource(unittest.TestCase): @@ -25,12 +31,13 @@ class TestRootResource(unittest.TestCase): self.mail_service.account_email = self.MAIL_ADDRESS root_resource = RootResource(self.services_factory) - 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): + self.root_resource._inbox_resource._html_template = "<html><head><title>$account_email</title></head></html>" + self.root_resource.initialize(provider=mock(), authenticator=mock()) + request = DummyRequest(['']) request.addCookie = lambda key, value: 'stubbed' @@ -88,6 +95,8 @@ class TestRootResource(unittest.TestCase): request.requestHeaders.setRawHeaders('x-xsrf-token', [csrf_token]) def test_should_unauthorize_child_resource_ajax_requests_when_csrf_mismatch(self): + self.root_resource.initialize(provider=mock(), authenticator=mock()) + request = DummyRequest(['/child']) request.method = 'POST' self._mock_ajax_csrf(request, 'stubbed csrf token') @@ -103,11 +112,41 @@ class TestRootResource(unittest.TestCase): d.addCallback(assert_unauthorized) return d + def test_GET_should_return_503_for_uninitialized_resource(self): + request = DummyRequest(['/sandbox/']) + request.method = 'GET' + + request.getCookie = MagicMock(return_value='stubbed csrf token') + + d = self.web.get(request) + + def assert_unavailable(_): + self.assertEqual(503, request.responseCode) + + d.addCallback(assert_unavailable) + return d + + def test_GET_should_return_404_for_non_existing_resource(self): + self.root_resource.initialize(provider=mock(), authenticator=mock()) + + request = DummyRequest(['/non-existing-child']) + request.method = 'GET' + request.getCookie = MagicMock(return_value='stubbed csrf token') + + d = self.web.get(request) + + def assert_not_found(_): + self.assertEqual(404, request.responseCode) + + d.addCallback(assert_not_found) + return d + def test_should_404_non_existing_resource_with_valid_csrf(self): + self.root_resource.initialize(provider=mock(), authenticator=mock()) + request = DummyRequest(['/non-existing-child']) request.method = 'POST' self._mock_ajax_csrf(request, 'stubbed csrf token') - request.getCookie = MagicMock(return_value='stubbed csrf token') d = self.web.get(request) @@ -123,7 +162,7 @@ class TestRootResource(unittest.TestCase): request = DummyRequest(['features']) request.getCookie = MagicMock(return_value='irrelevant -- stubbed') - self.root_resource._child_resources.add('features', FeaturesResource()) + self.root_resource.putChild('features', FeaturesResource()) self.root_resource._mode = MODE_RUNNING d = self.web.get(request) @@ -135,6 +174,8 @@ class TestRootResource(unittest.TestCase): return d def test_should_unauthorize_child_resource_non_ajax_POST_requests_when_csrf_input_mismatch(self): + self.root_resource.initialize(provider=mock(), authenticator=mock()) + request = DummyRequest(['mails']) request.method = 'POST' request.addArg('csrftoken', 'some csrf token') @@ -152,3 +193,45 @@ class TestRootResource(unittest.TestCase): d.addCallback(assert_unauthorized) return d + + def test_assets_should_be_publicly_available(self): + self.root_resource.initialize(provider=mock(), authenticator=mock()) + + request = DummyRequest(['assets', 'dummy.json']) + d = self.web.get(request) + + def assert_response(_): + self.assertEqual(200, request.responseCode) + + d.addCallback(assert_response) + return d + + def test_login_should_be_publicly_available(self): + self.root_resource.initialize(provider=mock(), authenticator=mock()) + + request = DummyRequest(['login']) + d = self.web.get(request) + + def assert_response(_): + self.assertEqual(200, request.responseCode) + + d.addCallback(assert_response) + return d + + def test_root_should_be_handled_by_inbox_resource(self): + request = DummyRequest([]) + request.prepath = [''] + request.path = '/' + # TODO: setup mocked portal + + resource = self.root_resource.getChildWithDefault(request.prepath[-1], request) + self.assertIsInstance(resource, InboxResource) + + def test_inbox_should_not_be_public(self): + request = DummyRequest([]) + request.prepath = [''] + request.path = '/' + # TODO: setup mocked portal + + resource = self.root_resource.getChildWithDefault(request.prepath[-1], request) + self.assertIsInstance(resource, InboxResource) |