summaryrefslogtreecommitdiff
path: root/service/pixelated/resources
diff options
context:
space:
mode:
authorFolker Bernitt <fbernitt@thoughtworks.com>2016-01-19 13:36:31 +0100
committerFolker Bernitt <fbernitt@thoughtworks.com>2016-01-22 11:00:22 +0100
commit995049a04fb15bd4e1cf27bf11e3be46f84e3bfe (patch)
tree27990273107b573b49f6af83c3a13ee63ae37b50 /service/pixelated/resources
parent7be15d9231a98f5cd439030ebc16361fb43287e9 (diff)
Add mutli-user mode to user-agent
- Issue #576 - To start in multi user, run with --multi-user --provider provider-name.tld
Diffstat (limited to 'service/pixelated/resources')
-rw-r--r--service/pixelated/resources/__init__.py29
-rw-r--r--service/pixelated/resources/auth.py177
-rw-r--r--service/pixelated/resources/login_resource.py105
-rw-r--r--service/pixelated/resources/root_resource.py21
-rw-r--r--service/pixelated/resources/session.py36
5 files changed, 364 insertions, 4 deletions
diff --git a/service/pixelated/resources/__init__.py b/service/pixelated/resources/__init__.py
index 3d81d784..9cde015f 100644
--- a/service/pixelated/resources/__init__.py
+++ b/service/pixelated/resources/__init__.py
@@ -16,8 +16,12 @@
import json
+from twisted.web._responses import UNAUTHORIZED
from twisted.web.resource import Resource
+# from pixelated.resources.login_resource import LoginResource
+from pixelated.resources.session import IPixelatedSession
+
class SetEncoder(json.JSONEncoder):
def default(self, obj):
@@ -48,8 +52,19 @@ class BaseResource(Resource):
self._services_factory = services_factory
def _get_user_id_from_request(self, request):
- # currently we are faking this
- return self._services_factory._services_by_user.keys()[0]
+ if self._services_factory.mode.is_single_user:
+ return None # it doesn't matter
+ session = self.get_session(request)
+ if session.is_logged_in():
+ return session.user_uuid
+ raise ValueError('Not logged in')
+
+ def is_logged_in(self, request):
+ session = self.get_session(request)
+ return session.is_logged_in()
+
+ def get_session(self, request):
+ return IPixelatedSession(request.getSession())
def _services(self, request):
user_id = self._get_user_id_from_request(request)
@@ -72,3 +87,13 @@ class BaseResource(Resource):
def feedback_service(self, request):
return self._service(request, 'feedback_service')
+
+
+class UnAuthorizedResource(Resource):
+
+ def __init__(self):
+ Resource.__init__(self)
+
+ def render_GET(self, request):
+ request.setResponseCode(UNAUTHORIZED)
+ return "Unauthorized!"
diff --git a/service/pixelated/resources/auth.py b/service/pixelated/resources/auth.py
new file mode 100644
index 00000000..7076490d
--- /dev/null
+++ b/service/pixelated/resources/auth.py
@@ -0,0 +1,177 @@
+#
+# Copyright (c) 2016 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# 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 logging
+
+from leap.auth import SRPAuth
+from leap.exceptions import SRPAuthenticationError
+from twisted.cred.checkers import ANONYMOUS
+from twisted.cred.credentials import ICredentials
+from twisted.cred.error import UnauthorizedLogin
+from twisted.internet import defer, threads
+from twisted.web._auth.wrapper import UnauthorizedResource
+from twisted.web.error import UnsupportedMethod
+from zope.interface import implements, implementer, Attribute
+from twisted.cred import portal, checkers, credentials
+from twisted.web import util
+from twisted.cred import error
+from twisted.web.resource import IResource, ErrorPage
+
+from pixelated.adapter.welcome_mail import add_welcome_mail
+from pixelated.config.leap import authenticate_user
+from pixelated.config.services import Services
+from pixelated.resources import IPixelatedSession
+
+
+log = logging.getLogger(__name__)
+
+
+@implementer(checkers.ICredentialsChecker)
+class LeapPasswordChecker(object):
+ credentialInterfaces = (
+ credentials.IUsernamePassword,
+ credentials.IUsernameHashedPassword
+ )
+
+ def __init__(self, setup_args, leap_provider):
+ self._setup_args = setup_args
+ self._leap_provider = leap_provider
+
+ def requestAvatarId(self, credentials):
+ def _validate_credentials():
+ try:
+ srp_auth = SRPAuth(self._leap_provider.api_uri, self._leap_provider.local_ca_crt)
+ srp_auth.authenticate(credentials.username, credentials.password)
+ except SRPAuthenticationError:
+ raise UnauthorizedLogin()
+
+ def _authententicate_user(_):
+ return authenticate_user(self._leap_provider, credentials.username, credentials.password)
+
+ d = threads.deferToThread(_validate_credentials)
+ d.addCallback(_authententicate_user)
+ return d
+
+
+class ISessionCredential(ICredentials):
+
+ request = Attribute('the current request')
+
+
+@implementer(ISessionCredential)
+class SessionCredential(object):
+ def __init__(self, request):
+ self.request = request
+
+
+@implementer(checkers.ICredentialsChecker)
+class SessionChecker(object):
+ credentialInterfaces = (ISessionCredential,)
+
+ def requestAvatarId(self, credentials):
+ session = self.get_session(credentials.request)
+ if session.is_logged_in():
+ return defer.succeed(session.user_uuid)
+ else:
+ return defer.succeed(ANONYMOUS)
+
+ def get_session(self, request):
+ return IPixelatedSession(request.getSession())
+
+
+class LeapUser(object):
+
+ def __init__(self, leap_session):
+ self._leap_session = leap_session
+
+ @defer.inlineCallbacks
+ def start_services(self, services_factory):
+ services = Services(self._leap_session)
+ yield services.setup()
+
+ if self._leap_session.fresh_account:
+ yield add_welcome_mail(self._leap_session.mail_store)
+
+ services_factory.add_session(self._leap_session.user_auth.uuid, services)
+
+ def init_http_session(self, request):
+ session = IPixelatedSession(request.getSession())
+ session.user_uuid = self._leap_session.user_auth.uuid
+
+
+class PixelatedRealm(object):
+ implements(portal.IRealm)
+
+ def __init__(self, root_resource, anonymous_resource):
+ self._root_resource = root_resource
+ self._anonymous_resource = anonymous_resource
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ if IResource in interfaces:
+ if avatarId == checkers.ANONYMOUS:
+ return IResource, checkers.ANONYMOUS, lambda: None
+ else:
+ leap_session = avatarId
+ user = LeapUser(leap_session)
+ return IResource, user, lambda: None
+ raise NotImplementedError()
+
+
+@implementer(IResource)
+class PixelatedAuthSessionWrapper(object):
+
+ isLeaf = False
+
+ def __init__(self, portal, root_resource, anonymous_resource, credentialFactories):
+ self._portal = portal
+ self._credentialFactories = credentialFactories
+ self._root_resource = root_resource
+ self._anonymous_resource = anonymous_resource
+
+ def render(self, request):
+ raise UnsupportedMethod(())
+
+ def getChildWithDefault(self, path, request):
+ request.postpath.insert(0, request.prepath.pop())
+
+ return self._authorizedResource(request)
+
+ def _authorizedResource(self, request):
+ creds = SessionCredential(request)
+ return util.DeferredResource(self._login(creds))
+
+ def _login(self, credentials):
+ d = self._portal.login(credentials, None, IResource)
+ d.addCallbacks(self._loginSucceeded, self._loginFailed)
+ return d
+
+ def _loginSucceeded(self, args):
+ interface, avatar, logout = args
+
+ if avatar == checkers.ANONYMOUS:
+ return self._anonymous_resource
+ else:
+ return self._root_resource
+
+ def _loginFailed(self, result):
+ if result.check(error.Unauthorized, error.LoginFailed):
+ return UnauthorizedResource(self._credentialFactories)
+ else:
+ log.err(
+ result,
+ "HTTPAuthSessionWrapper.getChildWithDefault encountered "
+ "unexpected error")
+ return ErrorPage(500, None, None)
diff --git a/service/pixelated/resources/login_resource.py b/service/pixelated/resources/login_resource.py
new file mode 100644
index 00000000..b0e8ac3b
--- /dev/null
+++ b/service/pixelated/resources/login_resource.py
@@ -0,0 +1,105 @@
+#
+# Copyright (c) 2016 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# 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 logging
+import os
+from string import Template
+
+from twisted.cred import credentials
+from twisted.internet import defer
+from twisted.web.resource import IResource
+from twisted.web.server import NOT_DONE_YET
+from twisted.web.static import File
+
+from pixelated.resources import BaseResource, UnAuthorizedResource
+
+log = logging.getLogger(__name__)
+
+
+class LoginResource(BaseResource):
+
+ def __init__(self, services_factory, portal=None):
+ BaseResource.__init__(self, services_factory)
+ self._static_folder = self._get_static_folder()
+ self._startup_folder = self._get_startup_folder()
+ self._html_template = open(os.path.join(self._startup_folder, 'login.html')).read()
+ self._portal = portal
+ self.putChild('startup-assets', File(self._startup_folder))
+
+ def set_portal(self, portal):
+ self._portal = portal
+
+ def getChild(self, path, request):
+ if path == '':
+ return self
+ if path == 'login':
+ return self
+ return UnAuthorizedResource()
+
+ def render_GET(self, request):
+ response = Template(self._html_template).safe_substitute()
+ return str(response)
+
+ def render_POST(self, request):
+
+ def render_response(response):
+ request.redirect("/")
+ request.finish()
+
+ def render_error(error):
+ login_form = self.render_GET(request)
+ request.status = 500
+ request.write('We got an error:\n')
+ request.write(str(error))
+ request.write(login_form)
+ request.finish()
+
+ d = self._handle_login(request)
+ d.addCallbacks(render_response, render_error)
+
+ return NOT_DONE_YET
+
+ @defer.inlineCallbacks
+ def _handle_login(self, request):
+ if self.is_logged_in(request):
+ defer.succeed(None)
+ return
+ username = request.args['username'][0]
+ password = request.args['password'][0]
+ creds = credentials.UsernamePassword(username, password)
+
+ iface, leap_user, logout = yield self._portal.login(creds, None, IResource)
+
+ # we should really check whether the response is anonymous
+
+ yield leap_user.start_services(self._services_factory)
+ leap_user.init_http_session(request)
+
+ log.info('about to redirect to home page')
+
+ def _get_startup_folder(self):
+ path = os.path.dirname(os.path.abspath(__file__))
+ return os.path.join(path, '..', 'assets')
+
+ def _get_static_folder(self):
+ static_folder = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "web-ui", "app"))
+ # 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", "app"))
+ if not os.path.exists(static_folder):
+ static_folder = os.path.join('/', 'usr', 'share', 'pixelated-user-agent')
+ return static_folder
diff --git a/service/pixelated/resources/root_resource.py b/service/pixelated/resources/root_resource.py
index 0894444b..a1ed876e 100644
--- a/service/pixelated/resources/root_resource.py
+++ b/service/pixelated/resources/root_resource.py
@@ -1,5 +1,20 @@
+#
+# Copyright (c) 2016 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# 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 os
-import requests
from string import Template
from pixelated.resources import BaseResource
@@ -7,6 +22,7 @@ from pixelated.resources.attachments_resource import AttachmentsResource
from pixelated.resources.contacts_resource import ContactsResource
from pixelated.resources.features_resource import FeaturesResource
from pixelated.resources.feedback_resource import FeedbackResource
+from pixelated.resources.login_resource import LoginResource
from pixelated.resources.user_settings_resource import UserSettingsResource
from pixelated.resources.mail_resource import MailResource
from pixelated.resources.mails_resource import MailsResource
@@ -39,7 +55,7 @@ class RootResource(BaseResource):
return self
return Resource.getChild(self, path, request)
- def initialize(self):
+ def initialize(self, portal=None):
self.putChild('assets', File(self._static_folder))
self.putChild('keys', KeysResource(self._services_factory))
self.putChild(AttachmentsResource.BASE_URL, AttachmentsResource(self._services_factory))
@@ -50,6 +66,7 @@ class RootResource(BaseResource):
self.putChild('mail', MailResource(self._services_factory))
self.putChild('feedback', FeedbackResource(self._services_factory))
self.putChild('user-settings', UserSettingsResource(self._services_factory))
+ self.putChild('login', LoginResource(self._services_factory, portal))
self._mode = MODE_RUNNING
diff --git a/service/pixelated/resources/session.py b/service/pixelated/resources/session.py
new file mode 100644
index 00000000..76b54901
--- /dev/null
+++ b/service/pixelated/resources/session.py
@@ -0,0 +1,36 @@
+#
+# Copyright (c) 2016 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+
+from zope.interface import Interface, Attribute, implements
+from twisted.python.components import registerAdapter
+from twisted.web.server import Session
+
+
+class IPixelatedSession(Interface):
+ user_uuid = Attribute('The uuid of the currently logged in user')
+
+
+class PixelatedSession(object):
+ implements(IPixelatedSession)
+
+ def __init__(self, session):
+ self.user_uuid = None
+
+ def is_logged_in(self):
+ return self.user_uuid is not None
+
+
+registerAdapter(PixelatedSession, Session, IPixelatedSession)