summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorKali Kaneko (leap communications) <kali@leap.se>2016-11-09 20:34:51 +0100
committerKali Kaneko (leap communications) <kali@leap.se>2016-12-29 03:09:50 +0100
commit86976a27e7aa9222afc0695c240b0ea7cc8e362b (patch)
tree8f19391d07b0f2e67b94f3f1e9bc168bd3e4cea8 /src
parentc3acb3ca45480d3a4d72731ca68b69bec6db4e2c (diff)
[feature] authentication classes and tests
Diffstat (limited to 'src')
-rw-r--r--src/leap/bitmask/core/_web.py121
-rw-r--r--src/leap/bitmask/core/dispatcher.py3
-rw-r--r--src/leap/bitmask/core/service.py8
3 files changed, 121 insertions, 11 deletions
diff --git a/src/leap/bitmask/core/_web.py b/src/leap/bitmask/core/_web.py
index 1cbba19..c0c041e 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 11a319f..5900390 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 9fde788..cba6f8d 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):