From c8c742cb981cbe4087a7863ebc3d2a8e3dd93f25 Mon Sep 17 00:00:00 2001 From: Victor Shyba Date: Tue, 22 Aug 2017 05:23:24 -0300 Subject: [feature] add a local realm with file auth checker -- Related: #8867 --- src/leap/soledad/server/_resource.py | 9 +++++ src/leap/soledad/server/auth.py | 73 ++++++++++++++++++++++++++++++++--- src/leap/soledad/server/entrypoint.py | 2 +- testing/tests/server/test_auth.py | 42 ++++++++++++++++++-- 4 files changed, 116 insertions(+), 10 deletions(-) diff --git a/src/leap/soledad/server/_resource.py b/src/leap/soledad/server/_resource.py index 28344b38..e4c51ded 100644 --- a/src/leap/soledad/server/_resource.py +++ b/src/leap/soledad/server/_resource.py @@ -50,6 +50,15 @@ class SoledadAnonResource(Resource): self.putChild('robots.txt', _Robots()) +class LocalResource(Resource): + """ + Used for localhost endpoints, like IncomingBox delivery. + """ + + def __init__(conf): + pass + + class SoledadResource(Resource): """ This is a dummy twisted resource, used only to allow different entry points diff --git a/src/leap/soledad/server/auth.py b/src/leap/soledad/server/auth.py index 1357b289..934517b5 100644 --- a/src/leap/soledad/server/auth.py +++ b/src/leap/soledad/server/auth.py @@ -39,6 +39,7 @@ from twisted.web.resource import IResource from leap.soledad.common.couch import couch_server from ._resource import SoledadResource, SoledadAnonResource +from ._resource import LocalResource from ._blobs import BlobsResource from ._config import get_config @@ -77,8 +78,63 @@ class SoledadRealm(object): raise NotImplementedError() +@implementer(IRealm) +class LocalServicesRealm(object): + + def __init__(self, conf=None): + if conf is None: + conf = get_config() + self.anon_resource = SoledadAnonResource( + enable_blobs=conf['blobs']) + self.auth_resource = LocalResource(conf) + + def requestAvatar(self, avatarId, mind, *interfaces): + + # Anonymous access + if IAnonymous.providedBy(avatarId): + return (IResource, self.anon_resource, + lambda: None) + + # Authenticated access + else: + if IResource in interfaces: + return (IResource, self.auth_resource, + lambda: None) + raise NotImplementedError() + + +@implementer(ICredentialsChecker) +class FileTokenChecker(object): + + def __init__(self, conf=None): + conf = conf or get_config() + self._trusted_services_tokens = {} + self._tokens_file_path = conf['services_tokens_file'] + self._reload_tokens() + + def _reload_tokens(self): + with open(self._tokens_file_path) as tokens_file: + for line in tokens_file.readlines(): + line = line.strip() + if not line.startswith('#'): + service, token = line.split(':') + self._trusted_services_tokens[service] = token + + def requestAvatarId(self, credentials): + if IAnonymous.providedBy(credentials): + return defer.succeed(Anonymous()) + + service = credentials.username + token = credentials.password + + if self._trusted_services_tokens[service] != token: + return defer.fail(error.UnauthorizedLogin()) + + return defer.succeed(service) + + @implementer(ICredentialsChecker) -class TokenChecker(object): +class CouchDBTokenChecker(object): credentialInterfaces = [IUsernamePassword, IAnonymous] @@ -164,10 +220,17 @@ class TokenCredentialFactory(object): raise error.LoginFailed('Invalid credentials') -def portalFactory(sync_pool): - realm = SoledadRealm(sync_pool=sync_pool) - checker = TokenChecker() - return Portal(realm, [checker]) +def portalFactory(public=True, sync_pool=None): + database_checker = CouchDBTokenChecker() + file_checker = FileTokenChecker() + if public: + assert sync_pool + realm = SoledadRealm(sync_pool=sync_pool) + auth_checkers = [database_checker] + else: + realm = LocalServicesRealm() + auth_checkers = [file_checker, database_checker] + return Portal(realm, auth_checkers) credentialFactory = TokenCredentialFactory() diff --git a/src/leap/soledad/server/entrypoint.py b/src/leap/soledad/server/entrypoint.py index c06b740e..9b9a5e66 100644 --- a/src/leap/soledad/server/entrypoint.py +++ b/src/leap/soledad/server/entrypoint.py @@ -40,7 +40,7 @@ class SoledadEntrypoint(SoledadSession): pool = threadpool.ThreadPool(name='wsgi') reactor.callWhenRunning(pool.start) reactor.addSystemEventTrigger('after', 'shutdown', pool.stop) - portal = portalFactory(pool) + portal = portalFactory(public=True, sync_pool=pool) SoledadSession.__init__(self, portal) diff --git a/testing/tests/server/test_auth.py b/testing/tests/server/test_auth.py index 6eb647ee..85dd5ecf 100644 --- a/testing/tests/server/test_auth.py +++ b/testing/tests/server/test_auth.py @@ -17,7 +17,9 @@ """ Tests for auth pieces. """ +import os import collections +import pytest from contextlib import contextmanager @@ -31,7 +33,8 @@ from twisted.web.test import test_httpauth import leap.soledad.server.auth as auth_module from leap.soledad.server.auth import SoledadRealm -from leap.soledad.server.auth import TokenChecker +from leap.soledad.server.auth import CouchDBTokenChecker +from leap.soledad.server.auth import FileTokenChecker from leap.soledad.server.auth import TokenCredentialFactory from leap.soledad.server._resource import SoledadResource @@ -66,7 +69,7 @@ def dummy_server(token): yield collections.defaultdict(lambda: DummyServer(token)) -class TokenCheckerTestCase(unittest.TestCase): +class DatabaseTokenCheckerTestCase(unittest.TestCase): @inlineCallbacks def test_good_creds(self): @@ -74,7 +77,7 @@ class TokenCheckerTestCase(unittest.TestCase): token = {'user_id': 'user', 'type': 'Token'} server = dummy_server(token) # setup the checker with the custom server - checker = TokenChecker() + checker = CouchDBTokenChecker() auth_module.couch_server = lambda url: server # assert the checker *can* verify the creds creds = UsernamePassword('user', 'pass') @@ -87,7 +90,7 @@ class TokenCheckerTestCase(unittest.TestCase): token = None server = dummy_server(token) # setup the checker with the custom server - checker = TokenChecker() + checker = CouchDBTokenChecker() auth_module.couch_server = lambda url: server # assert the checker *cannot* verify the creds creds = UsernamePassword('user', '') @@ -95,6 +98,37 @@ class TokenCheckerTestCase(unittest.TestCase): yield checker.requestAvatarId(creds) +class FileTokenCheckerTestCase(unittest.TestCase): + + @inlineCallbacks + @pytest.mark.usefixtures("method_tmpdir") + def test_good_creds(self): + auth_file_path = os.path.join(self.tempdir, 'auth.file') + with open(auth_file_path, 'w') as tempfile: + tempfile.write('goodservice:goodtoken') + # setup the checker with the auth tokens file + conf = {'services_tokens_file': auth_file_path} + checker = FileTokenChecker(conf) + # assert the checker *can* verify the creds + creds = UsernamePassword('goodservice', 'goodtoken') + avatarId = yield checker.requestAvatarId(creds) + self.assertEqual('goodservice', avatarId) + + @inlineCallbacks + @pytest.mark.usefixtures("method_tmpdir") + def test_bad_creds(self): + auth_file_path = os.path.join(self.tempdir, 'auth.file') + with open(auth_file_path, 'w') as tempfile: + tempfile.write('service:token') + # setup the checker with the auth tokens file + conf = {'services_tokens_file': auth_file_path} + checker = FileTokenChecker(conf) + # assert the checker *cannot* verify the creds + creds = UsernamePassword('service', 'wrongtoken') + with self.assertRaises(UnauthorizedLogin): + yield checker.requestAvatarId(creds) + + class TokenCredentialFactoryTestcase( test_httpauth.RequestMixin, test_httpauth.BasicAuthTestsMixin, unittest.TestCase): -- cgit v1.2.3