From 86976a27e7aa9222afc0695c240b0ea7cc8e362b Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Wed, 9 Nov 2016 20:34:51 +0100 Subject: [feature] authentication classes and tests --- src/leap/bitmask/core/_web.py | 121 +++++++++++++++++++++++++++++++++--- src/leap/bitmask/core/dispatcher.py | 3 +- src/leap/bitmask/core/service.py | 8 ++- 3 files changed, 121 insertions(+), 11 deletions(-) (limited to 'src/leap/bitmask/core') diff --git a/src/leap/bitmask/core/_web.py b/src/leap/bitmask/core/_web.py index 1cbba196..c0c041ee 100644 --- a/src/leap/bitmask/core/_web.py +++ b/src/leap/bitmask/core/_web.py @@ -23,14 +23,18 @@ import json import os import pkg_resources -from twisted.internet import reactor from twisted.application import service from twisted.internet import endpoints -from twisted.web.resource import Resource +from twisted.cred import portal, checkers, credentials, error as credError +from twisted.internet import reactor, defer +from twisted.logger import Logger +from twisted.web.guard import HTTPAuthSessionWrapper, BasicCredentialFactory +from twisted.web.resource import IResource, Resource from twisted.web.server import Site, NOT_DONE_YET from twisted.web.static import File -from twisted.logger import Logger + +from zope.interface import implementer from leap.bitmask.util import here from leap.bitmask.core.dispatcher import CommandDispatcher @@ -49,6 +53,90 @@ except Exception: log = Logger() +class TokenCredentialFactory(BasicCredentialFactory): + scheme = 'token' + + +@implementer(checkers.ICredentialsChecker) +class TokenDictChecker: + + credentialInterfaces = (credentials.IUsernamePassword, + credentials.IUsernameHashedPassword) + + def __init__(self, tokens): + "tokens: a dict-like object mapping usernames to session-tokens" + self.tokens = tokens + + def requestAvatarId(self, credentials): + username = credentials.username + if username in self.tokens: + if credentials.checkPassword(self.tokens[username]): + return defer.succeed(username) + else: + return defer.fail( + credError.UnauthorizedLogin("Bad session token")) + else: + return defer.fail( + credError.UnauthorizedLogin("No such user")) + + +@implementer(portal.IRealm) +class HttpPasswordRealm(object): + + def __init__(self, resource): + self.resource = resource + + def requestAvatar(self, user, mind, *interfaces): + if IResource in interfaces: + # the resource is passed on regardless of user + return (IResource, self.resource, lambda: None) + raise NotImplementedError() + + +@implementer(IResource) +class WhitelistHTTPAuthSessionWrapper(HTTPAuthSessionWrapper): + + """ + Wrap a portal, enforcing supported header-based authentication schemes. + It doesn't apply the enforcement to routes included in a whitelist. + """ + + # TODO extend this to inspect the data -- so that we pass a tuple + # with the action + + whitelist = (None,) + + def __init__(self, *args, **kw): + self.whitelist = kw.pop('whitelist', tuple()) + super(WhitelistHTTPAuthSessionWrapper, self).__init__( + *args, **kw) + + def getChildWithDefault(self, path, request): + if request.path in self.whitelist: + return self + return HTTPAuthSessionWrapper.getChildWithDefault(self, path, request) + + def render(self, request): + if request.path in self.whitelist: + _res = self._portal.realm.resource + return _res.render(request) + return HTTPAuthSessionWrapper.render(self, request) + + + +def protectedResourceFactory(resource, passwords, whitelist): + realm = HttpPasswordRealm(resource) + # TODO this should have the per-site tokens. + # can put it inside the API Resource object. + checker = PasswordDictChecker(passwords) + resource_portal = portal.Portal(realm, [checker]) + credentialFactory = TokenCredentialFactory('localhost') + protected_resource = WhitelistHTTPAuthSessionWrapper( + resource_portal, [credentialFactory], + whitelist=whitelist) + return protected_resource + + class HTTPDispatcherService(service.Service): """ @@ -58,11 +146,16 @@ class HTTPDispatcherService(service.Service): If the package ``leap.bitmask_js`` is found in the import path, we'll serve the whole JS UI in the root resource too (under the ``public`` path). - + If that package cannot be found, we'll serve just the javascript wrapper around the REST API. """ + API_WHITELIST = ( + '/API/bonafide/user', + ) + + def __init__(self, core, port=7070, debug=False, onion=False): self._core = core self.port = port @@ -71,6 +164,7 @@ class HTTPDispatcherService(service.Service): self.uri = '' def startService(self): + # TODO refactor this, too long---------------------------------------- if HAS_WEB_UI: webdir = os.path.abspath( pkg_resources.resource_filename('leap.bitmask_js', 'public')) @@ -83,18 +177,27 @@ class HTTPDispatcherService(service.Service): here(), '..', '..', '..', 'ui', 'app', 'lib', 'bitmask.js') jsapi = File(os.path.abspath(jspath)) + root = File(webdir) + # TODO move this to the tests... + DUMMY_PASS = {'user1': 'pass'} + api = Api(CommandDispatcher(self._core)) - root.putChild(u'API', api) + protected_api = protectedResourceFactory( + api, DUMMY_PASS, self.API_WHITELIST) + root.putChild(u'API', protected_api) + + if not HAS_WEB_UI: + root.putChild('bitmask.js', jsapi) # TODO --- pass requestFactory for header authentication + # so that we remove the setting of the cookie. + + # http://www.tsheffler.com/blog/2011/09/22/twisted-learning-about-cred-and-basicdigest-authentication/#Digest_Authentication factory = Site(root) self.site = factory - if not HAS_WEB_UI: - root.putChild('bitmask.js', jsapi) - if self.onion: try: import txtorcon @@ -102,13 +205,13 @@ class HTTPDispatcherService(service.Service): log.error('onion is enabled, but could not find txtorcon') return self._start_onion_service(factory) - else: interface = '127.0.0.1' endpoint = endpoints.TCP4ServerEndpoint( reactor, self.port, interface=interface) self.uri = 'https://%s:%s' % (interface, self.port) endpoint.listen(factory) + # TODO this should be set in a callback to the listen call self.running = True diff --git a/src/leap/bitmask/core/dispatcher.py b/src/leap/bitmask/core/dispatcher.py index 11a319fc..59003906 100644 --- a/src/leap/bitmask/core/dispatcher.py +++ b/src/leap/bitmask/core/dispatcher.py @@ -43,6 +43,7 @@ class SubCommand(object): __metaclass__ = APICommand def dispatch(self, service, *parts, **kw): + subcmd = '' try: subcmd = parts[1] _method = getattr(self, 'do_' + subcmd.upper(), None) @@ -402,7 +403,7 @@ class CommandDispatcher(object): return d def do_WEBUI(self, *parts): - subcmd = parts[1] + subcmd = parts[1] if parts and len(parts) > 1 else None dispatch = self.subcommand_webui.dispatch if subcmd == 'enable': diff --git a/src/leap/bitmask/core/service.py b/src/leap/bitmask/core/service.py index 9fde7889..cba6f8d9 100644 --- a/src/leap/bitmask/core/service.py +++ b/src/leap/bitmask/core/service.py @@ -51,6 +51,12 @@ else: class BitmaskBackend(configurable.ConfigurableService): + """ + The Bitmask Core Backend Service. + Here is where the multiple service tree gets composed. + This is passed to the command dispatcher. + """ + def __init__(self, basedir=configurable.DEFAULT_BASEDIR): configurable.ConfigurableService.__init__(self, basedir) @@ -217,7 +223,7 @@ class BitmaskBackend(configurable.ConfigurableService): class BackendCommands(object): """ - General commands for the BitmaskBackend Core Service. + General Commands for the BitmaskBackend Core Service. """ def __init__(self, core): -- cgit v1.2.3