From 91e4481c450eb7eb928debc1cb7fa59bdb63dd7b Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 25 Jul 2017 11:40:11 -0400 Subject: [pkg] packaging and path changes - move all the pixelated python package under src/ - move the pixelated_www package under the leap namespace - allow to set globally the static folder - add hours and minutes to the timestamp in package version, to allow for several releases a day. --- service/src/pixelated/resources/__init__.py | 159 ++++++++++++++ .../resources/account_recovery_resource.py | 87 ++++++++ .../pixelated/resources/attachments_resource.py | 110 ++++++++++ service/src/pixelated/resources/auth.py | 117 ++++++++++ .../pixelated/resources/backup_account_resource.py | 79 +++++++ .../src/pixelated/resources/contacts_resource.py | 44 ++++ .../src/pixelated/resources/features_resource.py | 46 ++++ .../src/pixelated/resources/feedback_resource.py | 31 +++ service/src/pixelated/resources/keys_resource.py | 46 ++++ service/src/pixelated/resources/login_resource.py | 173 +++++++++++++++ service/src/pixelated/resources/logout_resource.py | 45 ++++ service/src/pixelated/resources/mail_resource.py | 92 ++++++++ service/src/pixelated/resources/mails_resource.py | 244 +++++++++++++++++++++ service/src/pixelated/resources/root_resource.py | 139 ++++++++++++ .../src/pixelated/resources/sandbox_resource.py | 37 ++++ service/src/pixelated/resources/session.py | 55 +++++ service/src/pixelated/resources/tags_resource.py | 38 ++++ .../pixelated/resources/user_settings_resource.py | 43 ++++ service/src/pixelated/resources/users.py | 30 +++ 19 files changed, 1615 insertions(+) create mode 100644 service/src/pixelated/resources/__init__.py create mode 100644 service/src/pixelated/resources/account_recovery_resource.py create mode 100644 service/src/pixelated/resources/attachments_resource.py create mode 100644 service/src/pixelated/resources/auth.py create mode 100644 service/src/pixelated/resources/backup_account_resource.py create mode 100644 service/src/pixelated/resources/contacts_resource.py create mode 100644 service/src/pixelated/resources/features_resource.py create mode 100644 service/src/pixelated/resources/feedback_resource.py create mode 100644 service/src/pixelated/resources/keys_resource.py create mode 100644 service/src/pixelated/resources/login_resource.py create mode 100644 service/src/pixelated/resources/logout_resource.py create mode 100644 service/src/pixelated/resources/mail_resource.py create mode 100644 service/src/pixelated/resources/mails_resource.py create mode 100644 service/src/pixelated/resources/root_resource.py create mode 100644 service/src/pixelated/resources/sandbox_resource.py create mode 100644 service/src/pixelated/resources/session.py create mode 100644 service/src/pixelated/resources/tags_resource.py create mode 100644 service/src/pixelated/resources/user_settings_resource.py create mode 100644 service/src/pixelated/resources/users.py (limited to 'service/src/pixelated/resources') diff --git a/service/src/pixelated/resources/__init__.py b/service/src/pixelated/resources/__init__.py new file mode 100644 index 00000000..58b56786 --- /dev/null +++ b/service/src/pixelated/resources/__init__.py @@ -0,0 +1,159 @@ +# +# Copyright (c) 2014 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 . + +import json +import os + +from twisted.web.http import UNAUTHORIZED +from twisted.web.resource import Resource +from twisted.logger import Logger + +from pixelated.resources.session import IPixelatedSession + +from twisted.web.http import INTERNAL_SERVER_ERROR, SERVICE_UNAVAILABLE + +log = Logger() + + +STATIC = None + + +class SetEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, set): + return list(obj) + return super(SetEncoder, self).default(obj) + + +def respond_json(entity, request, status_code=200): + json_response = json.dumps(entity, cls=SetEncoder) + request.responseHeaders.setRawHeaders(b"content-type", [b"application/json"]) + request.code = status_code + return json_response + + +def respond_json_deferred(entity, request, status_code=200): + json_response = json.dumps(entity, cls=SetEncoder) + request.responseHeaders.setRawHeaders(b"content-type", [b"application/json"]) + request.code = status_code + request.write(json_response) + request.finish() + + +def handle_error_deferred(e, request): + log.error(e) + request.setResponseCode(INTERNAL_SERVER_ERROR) + request.write('Something went wrong!') + request.finish() + + +def set_static_folder(static_folder): + global STATIC + STATIC = static_folder + + +def get_protected_static_folder(static_folder=None): + static = static_folder or _get_static_folder() + return os.path.join(static, 'protected') + + +def get_public_static_folder(static_folder=None): + static = static_folder or _get_static_folder() + return os.path.join(static, 'public') + + +def _get_static_folder(): + if not STATIC: + static_folder = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "..", "web-ui", "dist")) + if not os.path.exists(static_folder): + static_folder = os.path.join('/', 'usr', 'share', 'pixelated-user-agent') + else: + static_folder = STATIC + return static_folder + + +class BaseResource(Resource): + + def __init__(self, services_factory): + Resource.__init__(self) + self._services_factory = services_factory + + def _get_user_id_from_request(self, request): + 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() and self._services_factory.has_session(session.user_uuid) + + def get_session(self, request): + return IPixelatedSession(request.getSession()) + + def is_admin(self, request): + services = self._services(request) + return services._leap_session.user_auth.is_admin() + + def _services(self, request): + user_id = self._get_user_id_from_request(request) + return self._services_factory.services(user_id) + + def _service(self, request, attribute): + return getattr(self._services(request), attribute) + + def keymanager(self, request): + return self._service(request, 'keymanager') + + def mail_service(self, request): + return self._service(request, 'mail_service') + + def search_engine(self, request): + return self._service(request, 'search_engine') + + def draft_service(self, request): + return self._service(request, 'draft_service') + + def feedback_service(self, request): + return self._service(request, 'feedback_service') + + def soledad(self, request): + return self._service(request, '_leap_session').soledad + + +class UnAuthorizedResource(Resource): + + def __init__(self): + Resource.__init__(self) + + def render_GET(self, request): + request.setResponseCode(UNAUTHORIZED) + return "Unauthorized!" + + def render_POST(self, request): + request.setResponseCode(UNAUTHORIZED) + return "Unauthorized!" + + +class UnavailableResource(Resource): + def __init__(self): + Resource.__init__(self) + + def render(self, request): + request.setResponseCode(SERVICE_UNAVAILABLE) + return "Service Unavailable" diff --git a/service/src/pixelated/resources/account_recovery_resource.py b/service/src/pixelated/resources/account_recovery_resource.py new file mode 100644 index 00000000..209a7693 --- /dev/null +++ b/service/src/pixelated/resources/account_recovery_resource.py @@ -0,0 +1,87 @@ +# +# Copyright (c) 2017 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 . + +import os +import json + +from twisted.python.filepath import FilePath +from twisted.web.http import OK, INTERNAL_SERVER_ERROR +from twisted.web.template import Element, XMLFile, renderElement +from twisted.web.server import NOT_DONE_YET +from twisted.internet import defer +from twisted.logger import Logger + +from pixelated.resources import BaseResource +from pixelated.resources import get_public_static_folder + +log = Logger() + + +class InvalidPasswordError(Exception): + pass + + +class AccountRecoveryPage(Element): + loader = XMLFile(FilePath(os.path.join(get_public_static_folder(), 'account_recovery.html'))) + + def __init__(self): + super(AccountRecoveryPage, self).__init__() + + +class AccountRecoveryResource(BaseResource): + BASE_URL = 'account-recovery' + isLeaf = True + + def __init__(self, services_factory): + BaseResource.__init__(self, services_factory) + + def render_GET(self, request): + request.setResponseCode(OK) + return self._render_template(request) + + def _render_template(self, request): + site = AccountRecoveryPage() + return renderElement(request, site) + + def render_POST(self, request): + def success_response(response): + request.setResponseCode(OK) + request.finish() + + def error_response(failure): + log.warn(failure) + request.setResponseCode(INTERNAL_SERVER_ERROR) + request.finish() + + d = self._handle_post(request) + d.addCallbacks(success_response, error_response) + return NOT_DONE_YET + + def _get_post_form(self, request): + return json.loads(request.content.getvalue()) + + def _validate_password(self, password, confirm_password): + return password == confirm_password and len(password) >= 8 and len(password) <= 9999 + + def _handle_post(self, request): + form = self._get_post_form(request) + password = form.get('password') + confirm_password = form.get('confirmPassword') + + if not self._validate_password(password, confirm_password): + return defer.fail(InvalidPasswordError('The user entered an invalid password or confirmation')) + + return defer.succeed('Done!') diff --git a/service/src/pixelated/resources/attachments_resource.py b/service/src/pixelated/resources/attachments_resource.py new file mode 100644 index 00000000..1081b4b8 --- /dev/null +++ b/service/src/pixelated/resources/attachments_resource.py @@ -0,0 +1,110 @@ +# +# Copyright (c) 2014 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 . +import cgi +import io +import re + +from twisted.internet import defer +from twisted.protocols.basic import FileSender +from twisted.python.log import msg +from twisted.web import server +from twisted.web.resource import Resource +from twisted.web.server import NOT_DONE_YET +from twisted.logger import Logger + +from pixelated.resources import respond_json_deferred, BaseResource + + +logger = Logger() + + +class AttachmentResource(Resource): + isLeaf = True + + def __init__(self, mail_service, attachment_id): + Resource.__init__(self) + self.attachment_id = attachment_id + self.mail_service = mail_service + + def render_GET(self, request): + def error_handler(failure): + msg(failure, 'attachment not found') + request.code = 404 + request.finish() + + encoding = request.args.get('encoding', [None])[0] + filename = request.args.get('filename', [self.attachment_id])[0] + content_type = request.args.get('content_type', ['application/octet-stream'])[0] + request.setHeader(b'Content-Type', content_type) + request.setHeader(b'Content-Disposition', bytes('attachment; filename="' + filename + '"')) + + d = self._send_attachment(encoding, filename, request) + d.addErrback(error_handler) + + return server.NOT_DONE_YET + + @defer.inlineCallbacks + def _send_attachment(self, encoding, filename, request): + attachment = yield self.mail_service.attachment(self.attachment_id) + + bytes_io = io.BytesIO(attachment['content']) + + try: + request.code = 200 + yield FileSender().beginFileTransfer(bytes_io, request) + finally: + bytes_io.close() + request.finish() + + def _extract_mimetype(self, content_type): + match = re.compile('([A-Za-z-]+\/[A-Za-z-]+)').search(content_type) + return match.group(1) + + +class AttachmentsResource(BaseResource): + BASE_URL = 'attachment' + + def __init__(self, services_factory): + BaseResource.__init__(self, services_factory) + + def getChild(self, attachment_id, request): + _mail_service = self.mail_service(request) + return AttachmentResource(_mail_service, attachment_id) + + def render_POST(self, request): + _mail_service = self.mail_service(request) + fields = cgi.FieldStorage(fp=request.content, headers=(request.getAllHeaders()), + environ={'REQUEST_METHOD': 'POST'}) + _file = fields['attachment'] + deferred = _mail_service.save_attachment(_file.value, _file.type) + + def send_location(attachment_id): + request.setHeader('Location', '/%s/%s' % (self.BASE_URL, attachment_id)) + response_json = {"ident": attachment_id, + "content-type": _file.type, + "encoding": "base64", # hard coded for now -- not really used + "name": _file.filename, + "size": len(_file.value)} + respond_json_deferred(response_json, request, status_code=201) + + def error_handler(error): + logger.error(error) + respond_json_deferred({"message": "Something went wrong. Attachment not saved."}, request, status_code=500) + + deferred.addCallback(send_location) + deferred.addErrback(error_handler) + + return NOT_DONE_YET diff --git a/service/src/pixelated/resources/auth.py b/service/src/pixelated/resources/auth.py new file mode 100644 index 00000000..adac985f --- /dev/null +++ b/service/src/pixelated/resources/auth.py @@ -0,0 +1,117 @@ +# +# 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 . + +import re + +from pixelated.resources import IPixelatedSession +from twisted.cred import error +from twisted.cred import portal, checkers +from twisted.cred.checkers import ANONYMOUS +from twisted.cred.credentials import ICredentials +from twisted.internet import defer +from twisted.logger import Logger +from twisted.web import util +from twisted.web._auth.wrapper import UnauthorizedResource +from twisted.web.error import UnsupportedMethod +from twisted.web.resource import IResource, ErrorPage +from zope.interface import implements, implementer, Attribute + + +log = Logger() + + +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 __init__(self, services_factory): + self._services_factory = services_factory + + def requestAvatarId(self, credentials): + session = self.get_session(credentials.request) + if session.is_logged_in() and self._services_factory.has_session(session.user_uuid): + return defer.succeed(session.user_uuid) + return defer.succeed(ANONYMOUS) + + def get_session(self, request): + return IPixelatedSession(request.getSession()) + + +class PixelatedRealm(object): + implements(portal.IRealm) + + def requestAvatar(self, avatarId, mind, *interfaces): + if IResource in interfaces: + return IResource, avatarId, 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, request)) + + def _login(self, credentials, request): + pattern = re.compile("^/sandbox/") + + def loginSucceeded(args): + interface, avatar, logout = args + if avatar == checkers.ANONYMOUS and not pattern.match(request.path): + return self._anonymous_resource + else: + return self._root_resource + + def loginFailed(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) + + d = self._portal.login(credentials, None, IResource) + d.addCallbacks(loginSucceeded, loginFailed) + return d diff --git a/service/src/pixelated/resources/backup_account_resource.py b/service/src/pixelated/resources/backup_account_resource.py new file mode 100644 index 00000000..94129122 --- /dev/null +++ b/service/src/pixelated/resources/backup_account_resource.py @@ -0,0 +1,79 @@ +# +# Copyright (c) 2017 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 . + +import os +import json + +from twisted.python.filepath import FilePath +from twisted.web.http import OK, NO_CONTENT, INTERNAL_SERVER_ERROR +from twisted.web.server import NOT_DONE_YET +from twisted.web.template import Element, XMLFile, renderElement + +from pixelated.resources import BaseResource +from pixelated.resources import get_protected_static_folder +from pixelated.account_recovery import AccountRecovery +from pixelated.support.language import parse_accept_language + + +class BackupAccountPage(Element): + loader = XMLFile(FilePath(os.path.join(get_protected_static_folder(), 'backup_account.html'))) + + def __init__(self): + super(BackupAccountPage, self).__init__() + + +class BackupAccountResource(BaseResource): + isLeaf = True + + def __init__(self, services_factory, authenticator, leap_provider): + BaseResource.__init__(self, services_factory) + self._authenticator = authenticator + self._leap_provider = leap_provider + + def render_GET(self, request): + request.setResponseCode(OK) + return self._render_template(request) + + def _render_template(self, request): + site = BackupAccountPage() + return renderElement(request, site) + + def render_POST(self, request): + account_recovery = AccountRecovery( + self._authenticator.bonafide_session, + self.soledad(request), + self._service(request, '_leap_session').smtp_config, + self._get_backup_email(request), + self._leap_provider.server_name, + language=self._get_language(request)) + + def update_response(response): + request.setResponseCode(NO_CONTENT) + request.finish() + + def error_response(response): + request.setResponseCode(INTERNAL_SERVER_ERROR) + request.finish() + + d = account_recovery.update_recovery_code() + d.addCallbacks(update_response, error_response) + return NOT_DONE_YET + + def _get_backup_email(self, request): + return json.loads(request.content.getvalue()).get('backupEmail') + + def _get_language(self, request): + return parse_accept_language(request.getAllHeaders()) diff --git a/service/src/pixelated/resources/contacts_resource.py b/service/src/pixelated/resources/contacts_resource.py new file mode 100644 index 00000000..dc17d1ac --- /dev/null +++ b/service/src/pixelated/resources/contacts_resource.py @@ -0,0 +1,44 @@ +# +# Copyright (c) 2014 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 . + +from pixelated.resources import respond_json_deferred, BaseResource +from twisted.internet.threads import deferToThread +from twisted.web import server +from twisted.web.resource import Resource + + +class ContactsResource(BaseResource): + + isLeaf = True + + def __init__(self, services_factory): + BaseResource.__init__(self, services_factory) + + def render_GET(self, request): + _search_engine = self.search_engine(request) + query = request.args.get('q', [''])[-1] + d = deferToThread(lambda: _search_engine.contacts(query)) + d.addCallback(lambda tags: respond_json_deferred(tags, request)) + + def handle_error(error): + print 'Something went wrong' + import traceback + traceback.print_exc() + print error + + d.addErrback(handle_error) + + return server.NOT_DONE_YET diff --git a/service/src/pixelated/resources/features_resource.py b/service/src/pixelated/resources/features_resource.py new file mode 100644 index 00000000..c1b61f12 --- /dev/null +++ b/service/src/pixelated/resources/features_resource.py @@ -0,0 +1,46 @@ +# +# Copyright (c) 2014 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 . + +from pixelated.resources import respond_json +import os +from twisted.web.resource import Resource + +from pixelated.resources.logout_resource import LogoutResource + + +class FeaturesResource(Resource): + DISABLED_FEATURES = ['draftReply'] + isLeaf = True + + def __init__(self, multi_user=False): + Resource.__init__(self) + self._multi_user = multi_user + + def render_GET(self, request): + disabled_features = self._disabled_features() + features = {'disabled_features': disabled_features} + self._add_multi_user_to(features) + return respond_json(features, request) + + def _disabled_features(self): + disabled_features = [default_disabled_feature for default_disabled_feature in self.DISABLED_FEATURES] + if not os.environ.get('FEEDBACK_URL'): + disabled_features.append('feedback') + return disabled_features + + def _add_multi_user_to(self, features): + if self._multi_user: + features.update({'multi_user': {'logout': LogoutResource.BASE_URL}}) diff --git a/service/src/pixelated/resources/feedback_resource.py b/service/src/pixelated/resources/feedback_resource.py new file mode 100644 index 00000000..aeead401 --- /dev/null +++ b/service/src/pixelated/resources/feedback_resource.py @@ -0,0 +1,31 @@ +# +# Copyright (c) 2015 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 . +import json + +from pixelated.resources import respond_json, BaseResource + + +class FeedbackResource(BaseResource): + isLeaf = True + + def __init__(self, services_factory): + BaseResource.__init__(self, services_factory) + + def render_POST(self, request): + _feedback_service = self.feedback_service(request) + feedback = json.loads(request.content.read()).get('feedback') + _feedback_service.open_ticket(feedback) + return respond_json({}, request) diff --git a/service/src/pixelated/resources/keys_resource.py b/service/src/pixelated/resources/keys_resource.py new file mode 100644 index 00000000..091c27d0 --- /dev/null +++ b/service/src/pixelated/resources/keys_resource.py @@ -0,0 +1,46 @@ +# +# Copyright (c) 2015 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 . + +from email.utils import parseaddr +from pixelated.resources import respond_json_deferred, BaseResource +from twisted.web import server + + +class KeysResource(BaseResource): + + isLeaf = True + + def __init__(self, services_factory): + BaseResource.__init__(self, services_factory) + + def render_GET(self, request): + _keymanager = self.keymanager(request) + + def finish_request(key): + if key.private: + respond_json_deferred(None, request, status_code=401) + else: + respond_json_deferred(key.get_active_json(), request) + + def key_not_found(_): + respond_json_deferred(None, request, status_code=404) + + _, key_to_find = parseaddr(request.args.get('search')[0]) + d = _keymanager.get_key(key_to_find) + d.addCallback(finish_request) + d.addErrback(key_not_found) + + return server.NOT_DONE_YET diff --git a/service/src/pixelated/resources/login_resource.py b/service/src/pixelated/resources/login_resource.py new file mode 100644 index 00000000..5b0b70d0 --- /dev/null +++ b/service/src/pixelated/resources/login_resource.py @@ -0,0 +1,173 @@ +# +# 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 . + +import os +from xml.sax import SAXParseException + +from pixelated.authentication import Authenticator +from pixelated.config.leap import BootstrapUserServices +from pixelated.resources import BaseResource, UnAuthorizedResource, IPixelatedSession +from pixelated.resources.account_recovery_resource import AccountRecoveryResource +from pixelated.resources import get_public_static_folder, respond_json +from pixelated.support.language import parse_accept_language + +from twisted.cred.error import UnauthorizedLogin +from twisted.internet import defer +from twisted.logger import Logger +from twisted.python.filepath import FilePath +from twisted.web import util +from twisted.web.http import UNAUTHORIZED, OK +from twisted.web.resource import NoResource +from twisted.web.server import NOT_DONE_YET +from twisted.web.static import File +from twisted.web.template import Element, XMLFile, renderElement, renderer + +log = Logger() + + +class DisclaimerElement(Element): + loader = XMLFile(FilePath(os.path.join(get_public_static_folder(), '_login_disclaimer_banner.html'))) + + def __init__(self, banner): + super(DisclaimerElement, self).__init__() + self._set_loader(banner) + self._banner_filename = banner or "_login_disclaimer_banner.html" + + def _set_loader(self, banner): + if banner: + current_path = os.path.dirname(os.path.abspath(__file__)) + banner_file_path = os.path.join(current_path, "..", "..", "..", banner) + self.loader = XMLFile(FilePath(banner_file_path)) + + def render(self, request): + try: + return super(DisclaimerElement, self).render(request) + except SAXParseException: + return ["Invalid XML template format for %s." % self._banner_filename] + except IOError: + return ["Disclaimer banner file %s could not be read or does not exit." % self._banner_filename] + + +class LoginWebSite(Element): + loader = XMLFile(FilePath(os.path.join(get_public_static_folder(), 'login.html'))) + + def __init__(self, disclaimer_banner_file=None): + super(LoginWebSite, self).__init__() + self.disclaimer_banner_file = disclaimer_banner_file + + @renderer + def disclaimer(self, request, tag): + return DisclaimerElement(self.disclaimer_banner_file).render(request) + + +class LoginResource(BaseResource): + BASE_URL = 'login' + + def __init__(self, services_factory, provider=None, disclaimer_banner=None, authenticator=None): + BaseResource.__init__(self, services_factory) + self._disclaimer_banner = disclaimer_banner + self._provider = provider + self._authenticator = authenticator + self._bootstrap_user_services = BootstrapUserServices(services_factory, provider) + + static_folder = get_public_static_folder() + self.putChild('public', File(static_folder)) + with open(os.path.join(static_folder, 'interstitial.html')) as f: + self.interstitial = f.read() + + def getChild(self, path, request): + if path == '': + return self + if path == 'login': + return self + if path == 'status': + return LoginStatusResource(self._services_factory) + if path == AccountRecoveryResource.BASE_URL: + return AccountRecoveryResource(self._services_factory) + if not self.is_logged_in(request): + return UnAuthorizedResource() + return NoResource() + + def render_GET(self, request): + request.setResponseCode(OK) + return self._render_template(request) + + def _render_template(self, request): + site = LoginWebSite(disclaimer_banner_file=self._disclaimer_banner) + return renderElement(request, site) + + def render_POST(self, request): + if self.is_logged_in(request): + return util.redirectTo("/", request) + + def render_response(user_auth): + request.setResponseCode(OK) + request.write(self.interstitial) + request.finish() + self._complete_bootstrap(user_auth, request) + + def render_error(error): + if error.type is UnauthorizedLogin: + log.info('Unauthorized login for %s. User typed wrong username/password combination.' % request.args['username'][0]) + else: + log.error('Authentication error for %s' % request.args['username'][0]) + log.error('%s' % error) + request.setResponseCode(UNAUTHORIZED) + content = util.redirectTo("/login?auth-error", request) + request.write(content) + request.finish() + + d = self._handle_login(request) + d.addCallbacks(render_response, render_error) + return NOT_DONE_YET + + @defer.inlineCallbacks + def _handle_login(self, request): + username = request.args['username'][0] + password = request.args['password'][0] + user_auth = yield self._authenticator.authenticate(username, password) + defer.returnValue(user_auth) + + def _complete_bootstrap(self, user_auth, request): + def login_error(error, session): + log.error('Login error during %s services setup: %s \n %s' % (user_auth.username, error.getErrorMessage(), error.getTraceback())) + session.login_error() + + def login_successful(_, session): + session.login_successful(user_auth.uuid) + + language = parse_accept_language(request.getAllHeaders()) + password = request.args['password'][0] + session = IPixelatedSession(request.getSession()) + session.login_started() + + d = self._bootstrap_user_services.setup(user_auth, password, language) + d.addCallback(login_successful, session) + d.addErrback(login_error, session) + + +class LoginStatusResource(BaseResource): + isLeaf = True + + def __init__(self, services_factory): + BaseResource.__init__(self, services_factory) + + def render_GET(self, request): + session = IPixelatedSession(request.getSession()) + status = 'completed' if self._services_factory.mode.is_single_user else str(session.check_login_status()) + + response = {'status': status} + return respond_json(response, request) diff --git a/service/src/pixelated/resources/logout_resource.py b/service/src/pixelated/resources/logout_resource.py new file mode 100644 index 00000000..a4fe584f --- /dev/null +++ b/service/src/pixelated/resources/logout_resource.py @@ -0,0 +1,45 @@ +# +# Copyright (c) 2014 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 . + +from twisted.internet import defer +from twisted.web import util +from twisted.web.server import NOT_DONE_YET + +from pixelated.resources import BaseResource, handle_error_deferred +from pixelated.resources.login_resource import LoginResource + + +class LogoutResource(BaseResource): + BASE_URL = "logout" + isLeaf = True + + @defer.inlineCallbacks + def _execute_logout(self, request): + http_session = self.get_session(request) + yield self._services_factory.destroy_session(http_session.user_uuid) + http_session.expire() + + def render_POST(self, request): + def _redirect_to_login(_): + content = util.redirectTo("/%s" % LoginResource.BASE_URL, request) + request.write(content) + request.finish() + + d = self._execute_logout(request) + d.addCallback(_redirect_to_login) + d.addErrback(handle_error_deferred, request) + + return NOT_DONE_YET diff --git a/service/src/pixelated/resources/mail_resource.py b/service/src/pixelated/resources/mail_resource.py new file mode 100644 index 00000000..e1ba6087 --- /dev/null +++ b/service/src/pixelated/resources/mail_resource.py @@ -0,0 +1,92 @@ +# +# Copyright (c) 2015 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 . + +import json + +from twisted.python.log import err +from twisted.web.resource import Resource +from twisted.web.server import NOT_DONE_YET + +from pixelated.resources import respond_json_deferred, BaseResource, handle_error_deferred +from pixelated.support import replier + + +class MailTags(Resource): + + isLeaf = True + + def __init__(self, mail_id, mail_service): + Resource.__init__(self) + self._mail_service = mail_service + self._mail_id = mail_id + + def render_POST(self, request): + new_tags = json.loads(request.content.read()).get('newtags') + + d = self._mail_service.update_tags(self._mail_id, new_tags) + d.addCallback(lambda mail: respond_json_deferred(mail.as_dict(), request)) + + def handle403(failure): + failure.trap(ValueError) + return respond_json_deferred(failure.getErrorMessage(), request, 403) + d.addErrback(handle403) + return NOT_DONE_YET + + +class Mail(Resource): + + def __init__(self, mail_id, mail_service): + Resource.__init__(self) + self.putChild('tags', MailTags(mail_id, mail_service)) + self._mail_id = mail_id + self._mail_service = mail_service + + def render_GET(self, request): + def populate_reply(mail): + mail_dict = mail.as_dict() + current_user = self._mail_service.account_email + sender = mail.headers.get('Reply-to', mail.headers.get('From')) + to = mail.headers.get('To', []) + ccs = mail.headers.get('Cc', []) + mail_dict['replying'] = replier.generate_recipients(sender, to, ccs, current_user) + return mail_dict + + d = self._mail_service.mail(self._mail_id) + d.addCallback(lambda mail: populate_reply(mail)) + d.addCallback(lambda mail_dict: respond_json_deferred(mail_dict, request)) + d.addErrback(handle_error_deferred, request) + + return NOT_DONE_YET + + def render_DELETE(self, request): + def response_failed(failure): + err(failure, 'something failed') + request.finish() + + d = self._mail_service.delete_mail(self._mail_id) + d.addCallback(lambda _: respond_json_deferred(None, request)) + d.addErrback(response_failed) + return NOT_DONE_YET + + +class MailResource(BaseResource): + + def __init__(self, services_factory): + BaseResource.__init__(self, services_factory) + + def getChild(self, mail_id, request): + _mail_service = self.mail_service(request) + return Mail(mail_id, _mail_service) diff --git a/service/src/pixelated/resources/mails_resource.py b/service/src/pixelated/resources/mails_resource.py new file mode 100644 index 00000000..d911e0d2 --- /dev/null +++ b/service/src/pixelated/resources/mails_resource.py @@ -0,0 +1,244 @@ +# +# Copyright (c) 2015 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 . + +import time +import json + +from twisted.internet import defer +from twisted.logger import Logger +from twisted.web.server import NOT_DONE_YET +from twisted.web.resource import Resource +from twisted.web import server + +from leap.common import events + +from pixelated.adapter.model.mail import InputMail +from pixelated.resources import respond_json_deferred, BaseResource +from pixelated.adapter.services.mail_sender import SMTPDownException +from pixelated.support.functional import to_unicode + + +log = Logger() + + +class MailsUnreadResource(Resource): + isLeaf = True + + def __init__(self, mail_service): + Resource.__init__(self) + self._mail_service = mail_service + + def render_POST(self, request): + idents = json.load(request.content).get('idents') + deferreds = [] + for ident in idents: + deferreds.append(self._mail_service.mark_as_unread(ident)) + + d = defer.gatherResults(deferreds, consumeErrors=True) + d.addCallback(lambda _: respond_json_deferred(None, request)) + d.addErrback(lambda _: respond_json_deferred(None, request, status_code=500)) + + return NOT_DONE_YET + + +class MailsReadResource(Resource): + isLeaf = True + + def __init__(self, mail_service): + Resource.__init__(self) + self._mail_service = mail_service + + def render_POST(self, request): + idents = json.load(request.content).get('idents') + deferreds = [] + for ident in idents: + deferreds.append(self._mail_service.mark_as_read(ident)) + + d = defer.gatherResults(deferreds, consumeErrors=True) + d.addCallback(lambda _: respond_json_deferred(None, request)) + d.addErrback(lambda _: respond_json_deferred(None, request, status_code=500)) + + return NOT_DONE_YET + + +class MailsDeleteResource(Resource): + isLeaf = True + + def __init__(self, mail_service): + Resource.__init__(self) + self._mail_service = mail_service + + def render_POST(self, request): + def response_failed(failure): + log.error('something failed: %s' % failure.getErrorMessage()) + request.finish() + + idents = json.loads(request.content.read())['idents'] + deferreds = [] + for ident in idents: + deferreds.append(self._mail_service.delete_mail(ident)) + + d = defer.gatherResults(deferreds, consumeErrors=True) + d.addCallback(lambda _: respond_json_deferred(None, request)) + d.addErrback(response_failed) + return NOT_DONE_YET + + +class MailsRecoverResource(Resource): + isLeaf = True + + def __init__(self, mail_service): + Resource.__init__(self) + self._mail_service = mail_service + + def render_POST(self, request): + idents = json.loads(request.content.read())['idents'] + deferreds = [] + for ident in idents: + deferreds.append(self._mail_service.recover_mail(ident)) + d = defer.gatherResults(deferreds, consumeErrors=True) + d.addCallback(lambda _: respond_json_deferred(None, request)) + d.addErrback(lambda _: respond_json_deferred(None, request, status_code=500)) + return NOT_DONE_YET + + +class MailsArchiveResource(Resource): + isLeaf = True + + def __init__(self, mail_service): + Resource.__init__(self) + self._mail_service = mail_service + + def render_POST(self, request): + idents = json.loads(request.content.read())['idents'] + deferreds = [] + for ident in idents: + deferreds.append(self._mail_service.archive_mail(ident)) + d = defer.gatherResults(deferreds, consumeErrors=True) + d.addCallback(lambda _: respond_json_deferred({'successMessage': 'your-message-was-archived'}, request)) + d.addErrback(lambda _: respond_json_deferred(None, request, status_code=500)) + return NOT_DONE_YET + + +class MailsResource(BaseResource): + + def _register_smtp_error_handler(self): + + def on_error(event, content): + delivery_error_mail = InputMail.delivery_error_template(delivery_address=event.content) + self._mail_service.mailboxes.inbox.add(delivery_error_mail) + + events.register(events.catalog.SMTP_SEND_MESSAGE_ERROR, callback=on_error) + + def __init__(self, services_factory): + BaseResource.__init__(self, services_factory) + self._register_smtp_error_handler() + + def getChild(self, action, request): + _mail_service = self.mail_service(request) + + if action == 'delete': + return MailsDeleteResource(_mail_service) + if action == 'recover': + return MailsRecoverResource(_mail_service) + if action == 'archive': + return MailsArchiveResource(_mail_service) + if action == 'read': + return MailsReadResource(_mail_service) + if action == 'unread': + return MailsUnreadResource(_mail_service) + + def _build_mails_response(self, (mails, total)): + return { + "stats": { + "total": total, + }, + "mails": [mail.as_dict() for mail in mails] + } + + def render_GET(self, request): + + _mail_service = self.mail_service(request) + query, window_size, page = request.args.get('q')[0], request.args.get('w')[0], request.args.get('p')[0] + unicode_query = to_unicode(query) + d = _mail_service.mails(unicode_query, window_size, page) + + d.addCallback(self._build_mails_response) + d.addCallback(lambda res: respond_json_deferred(res, request)) + + def error_handler(error): + print error + + d.addErrback(error_handler) + + return NOT_DONE_YET + + def render_POST(self, request): + def onError(error): + if isinstance(error.value, SMTPDownException): + respond_json_deferred({'message': str(error.value)}, request, status_code=503) + else: + log.error('error occurred while sending: %s' % error.getErrorMessage()) + respond_json_deferred({'message': 'an error occurred while sending'}, request, status_code=422) + + deferred = self._handle_post(request) + deferred.addErrback(onError) + + return server.NOT_DONE_YET + + def render_PUT(self, request): + def onError(error): + log.error('error saving draft: %s' % error.getErrorMessage()) + respond_json_deferred("", request, status_code=422) + + deferred = self._handle_put(request) + deferred.addErrback(onError) + + return server.NOT_DONE_YET + + @defer.inlineCallbacks + def _fetch_attachment_contents(self, content_dict, _mail_service): + attachments = content_dict.get('attachments', []) if content_dict else [] + for attachment in attachments: + retrieved_attachment = yield _mail_service.attachment(attachment['ident']) + attachment['raw'] = retrieved_attachment['content'] + content_dict['attachments'] = attachments + defer.returnValue(content_dict) + + @defer.inlineCallbacks + def _handle_post(self, request): + _mail_service = self.mail_service(request) + content_dict = json.loads(request.content.read()) + with_attachment_content = yield self._fetch_attachment_contents(content_dict, _mail_service) + + sent_mail = yield _mail_service.send_mail(with_attachment_content) + respond_json_deferred(sent_mail.as_dict(), request, status_code=201) + + @defer.inlineCallbacks + def _handle_put(self, request): + _draft_service = self.draft_service(request) + _mail_service = self.mail_service(request) + content_dict = json.loads(request.content.read()) + with_attachment_content = yield self._fetch_attachment_contents(content_dict, _mail_service) + + _mail = InputMail.from_dict(with_attachment_content, from_address=_mail_service.account_email) + draft_id = content_dict.get('ident') + pixelated_mail = yield _draft_service.process_draft(draft_id, _mail) + + if not pixelated_mail: + respond_json_deferred("", request, status_code=422) + else: + respond_json_deferred({'ident': pixelated_mail.ident}, request) diff --git a/service/src/pixelated/resources/root_resource.py b/service/src/pixelated/resources/root_resource.py new file mode 100644 index 00000000..b014a590 --- /dev/null +++ b/service/src/pixelated/resources/root_resource.py @@ -0,0 +1,139 @@ +# +# 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 . +import hashlib +import json +import os +from string import Template +from pixelated.resources.users import UsersResource + +from pixelated.resources import BaseResource, UnAuthorizedResource, UnavailableResource +from pixelated.resources import get_public_static_folder, get_protected_static_folder +from pixelated.resources.attachments_resource import AttachmentsResource +from pixelated.resources.sandbox_resource import SandboxResource +from pixelated.resources.account_recovery_resource import AccountRecoveryResource +from pixelated.resources.backup_account_resource import BackupAccountResource +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, LoginStatusResource +from pixelated.resources.logout_resource import LogoutResource +from pixelated.resources.user_settings_resource import UserSettingsResource +from pixelated.resources.mail_resource import MailResource +from pixelated.resources.mails_resource import MailsResource +from pixelated.resources.tags_resource import TagsResource +from pixelated.resources.keys_resource import KeysResource +from twisted.web.resource import NoResource +from twisted.web.static import File + +from twisted.logger import Logger + +log = Logger() + + +CSRF_TOKEN_LENGTH = 32 + +MODE_STARTUP = 1 +MODE_RUNNING = 2 + + +class RootResource(BaseResource): + def __init__(self, services_factory, static_folder=None): + BaseResource.__init__(self, services_factory) + self._public_static_folder = get_public_static_folder(static_folder) + self._protected_static_folder = get_protected_static_folder(static_folder) + self._html_template = open(os.path.join(self._protected_static_folder, 'index.html')).read() + self._services_factory = services_factory + self._child_resources = ChildResourcesMap() + with open(os.path.join(self._public_static_folder, 'interstitial.html')) as f: + self.interstitial = f.read() + self._startup_mode() + + def _startup_mode(self): + self.putChild('public', File(self._public_static_folder)) + self.putChild('status', LoginStatusResource(self._services_factory)) + self._mode = MODE_STARTUP + + def getChild(self, path, request): + if path == '': + return self + if self._mode == MODE_STARTUP: + return UnavailableResource() + if self._is_xsrf_valid(request): + return self._child_resources.get(path) + return UnAuthorizedResource() + + def _is_xsrf_valid(self, request): + get_request = (request.method == 'GET') + if get_request: + return True + + xsrf_token = request.getCookie('XSRF-TOKEN') + + ajax_request = (request.getHeader('x-requested-with') == 'XMLHttpRequest') + if ajax_request: + xsrf_header = request.getHeader('x-xsrf-token') + return xsrf_header and xsrf_header == xsrf_token + + csrf_input = request.args.get('csrftoken', [None])[0] or json.loads(request.content.read()).get('csrftoken', [None])[0] + return csrf_input and csrf_input == xsrf_token + + def initialize(self, provider=None, disclaimer_banner=None, authenticator=None): + self._child_resources.add('assets', File(self._protected_static_folder)) + self._child_resources.add(AccountRecoveryResource.BASE_URL, AccountRecoveryResource(self._services_factory)) + self._child_resources.add('backup-account', BackupAccountResource(self._services_factory, authenticator, provider)) + self._child_resources.add('sandbox', SandboxResource(self._protected_static_folder)) + self._child_resources.add('keys', KeysResource(self._services_factory)) + self._child_resources.add(AttachmentsResource.BASE_URL, AttachmentsResource(self._services_factory)) + self._child_resources.add('contacts', ContactsResource(self._services_factory)) + self._child_resources.add('features', FeaturesResource(provider)) + self._child_resources.add('tags', TagsResource(self._services_factory)) + self._child_resources.add('mails', MailsResource(self._services_factory)) + self._child_resources.add('mail', MailResource(self._services_factory)) + self._child_resources.add('feedback', FeedbackResource(self._services_factory)) + self._child_resources.add('user-settings', UserSettingsResource(self._services_factory)) + self._child_resources.add('users', UsersResource(self._services_factory)) + self._child_resources.add(LoginResource.BASE_URL, + LoginResource(self._services_factory, provider, disclaimer_banner=disclaimer_banner, authenticator=authenticator)) + self._child_resources.add(LogoutResource.BASE_URL, LogoutResource(self._services_factory)) + + self._mode = MODE_RUNNING + + def _is_starting(self): + return self._mode == MODE_STARTUP + + def _add_csrf_cookie(self, request): + csrf_token = hashlib.sha256(os.urandom(CSRF_TOKEN_LENGTH)).hexdigest() + request.addCookie('XSRF-TOKEN', csrf_token) + + def render_GET(self, request): + self._add_csrf_cookie(request) + if self._is_starting(): + return self.interstitial + else: + account_email = self.mail_service(request).account_email + response = Template(self._html_template).safe_substitute(account_email=account_email) + return str(response) + + +class ChildResourcesMap(object): + def __init__(self): + self._registry = {} + + def add(self, path, resource): + self._registry[path] = resource + + def get(self, path): + return self._registry.get(path) or NoResource() diff --git a/service/src/pixelated/resources/sandbox_resource.py b/service/src/pixelated/resources/sandbox_resource.py new file mode 100644 index 00000000..35f99774 --- /dev/null +++ b/service/src/pixelated/resources/sandbox_resource.py @@ -0,0 +1,37 @@ +# +# 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 . + +from twisted.web.static import File + + +class SandboxResource(File): + CSP_HEADER_VALUES = "sandbox allow-popups allow-scripts;" \ + "default-src 'self';" \ + "style-src *;" \ + "script-src *;" \ + "font-src *;" \ + "img-src *;" \ + "object-src 'none';" \ + "connect-src 'none';" + + def render_GET(self, request): + request.setHeader('Content-Security-Policy', self.CSP_HEADER_VALUES) + request.setHeader('X-Content-Security-Policy', self.CSP_HEADER_VALUES) + request.setHeader('X-Webkit-CSP', self.CSP_HEADER_VALUES) + request.setHeader('Access-Control-Allow-Origin', '*') + request.setHeader('Access-Control-Allow-Methods', 'GET') + + return super(SandboxResource, self).render_GET(request) diff --git a/service/src/pixelated/resources/session.py b/service/src/pixelated/resources/session.py new file mode 100644 index 00000000..5dfa52e6 --- /dev/null +++ b/service/src/pixelated/resources/session.py @@ -0,0 +1,55 @@ +# +# 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 . + +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') + login_status = Attribute('The status during user login') + + +class PixelatedSession(object): + implements(IPixelatedSession) + + def __init__(self, session): + self.user_uuid = None + self.login_status = None + + def is_logged_in(self): + return self.user_uuid is not None + + def expire(self): + self.user_uuid = None + self.login_status = None + + def login_started(self): + self.login_status = 'started' + + def login_successful(self, user_uuid): + self.user_uuid = user_uuid + self.login_status = 'completed' + + def login_error(self): + self.login_status = 'error' + + def check_login_status(self): + return self.login_status + + +registerAdapter(PixelatedSession, Session, IPixelatedSession) diff --git a/service/src/pixelated/resources/tags_resource.py b/service/src/pixelated/resources/tags_resource.py new file mode 100644 index 00000000..4cea4ca7 --- /dev/null +++ b/service/src/pixelated/resources/tags_resource.py @@ -0,0 +1,38 @@ +# +# Copyright (c) 2014 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 . + +from pixelated.resources import respond_json_deferred, BaseResource, handle_error_deferred +from twisted.internet.threads import deferToThread +from twisted.web.server import NOT_DONE_YET + + +class TagsResource(BaseResource): + + isLeaf = True + + def __init__(self, services_factory): + BaseResource.__init__(self, services_factory) + + def render_GET(self, request): + _search_engine = self.search_engine(request) + query = request.args.get('q', [''])[0] + skip_default_tags = request.args.get('skipDefaultTags', [False])[0] + + d = deferToThread(lambda: _search_engine.tags(query=query, skip_default_tags=skip_default_tags)) + d.addCallback(lambda tags: respond_json_deferred(tags, request)) + d.addErrback(handle_error_deferred, request) + + return NOT_DONE_YET diff --git a/service/src/pixelated/resources/user_settings_resource.py b/service/src/pixelated/resources/user_settings_resource.py new file mode 100644 index 00000000..04b434bd --- /dev/null +++ b/service/src/pixelated/resources/user_settings_resource.py @@ -0,0 +1,43 @@ +# +# Copyright (c) 2015 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 . + +from pixelated.resources import respond_json_deferred, BaseResource +from twisted.web import server + +FINGERPRINT_NOT_FOUND = 'Fingerprint not found' + + +class UserSettingsResource(BaseResource): + isLeaf = True + + def __init__(self, services_factory): + BaseResource.__init__(self, services_factory) + + def render_GET(self, request): + _account_email = self.mail_service(request).account_email + + def finish_request(key): + _fingerprint = key.fingerprint + respond_json_deferred({'account_email': _account_email, 'fingerprint': _fingerprint}, request) + + def key_not_found(_): + respond_json_deferred({'account_email': _account_email, 'fingerprint': FINGERPRINT_NOT_FOUND}, request) + + d = self.keymanager(request).get_key(_account_email) + d.addCallback(finish_request) + d.addErrback(key_not_found) + + return server.NOT_DONE_YET diff --git a/service/src/pixelated/resources/users.py b/service/src/pixelated/resources/users.py new file mode 100644 index 00000000..a3e6118e --- /dev/null +++ b/service/src/pixelated/resources/users.py @@ -0,0 +1,30 @@ +# +# 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 . + +from pixelated.resources import respond_json_deferred, BaseResource, respond_json, UnAuthorizedResource +from twisted.web import server + + +class UsersResource(BaseResource): + isLeaf = True + + def __init__(self, services_factory): + BaseResource.__init__(self, services_factory) + + def render_GET(self, request): + if self.is_admin(request): + return respond_json({"count": self._services_factory.online_sessions()}, request) + return UnAuthorizedResource().render_GET(request) -- cgit v1.2.3