diff options
Diffstat (limited to 'service/pixelated/resources')
-rw-r--r-- | service/pixelated/resources/__init__.py | 29 | ||||
-rw-r--r-- | service/pixelated/resources/auth.py | 177 | ||||
-rw-r--r-- | service/pixelated/resources/login_resource.py | 105 | ||||
-rw-r--r-- | service/pixelated/resources/root_resource.py | 21 | ||||
-rw-r--r-- | service/pixelated/resources/session.py | 36 |
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) |