summaryrefslogtreecommitdiff
path: root/service/pixelated
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
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')
-rw-r--r--service/pixelated/adapter/welcome_mail.py28
-rw-r--r--service/pixelated/application.py143
-rw-r--r--service/pixelated/assets/favicon.pngbin0 -> 592 bytes
-rw-r--r--service/pixelated/assets/hive-bg.pngbin0 -> 3356 bytes
-rw-r--r--service/pixelated/assets/login.html32
-rw-r--r--service/pixelated/assets/normalize.min.css1
-rw-r--r--service/pixelated/assets/opensans.css69
-rw-r--r--service/pixelated/assets/pixelated-logo-orange.svg29
-rw-r--r--service/pixelated/assets/pixelated.css127
-rw-r--r--service/pixelated/config/arguments.py3
-rw-r--r--service/pixelated/config/credentials.py1
-rw-r--r--service/pixelated/config/leap.py61
-rw-r--r--service/pixelated/config/services.py4
-rw-r--r--service/pixelated/maintenance.py4
-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
19 files changed, 802 insertions, 68 deletions
diff --git a/service/pixelated/adapter/welcome_mail.py b/service/pixelated/adapter/welcome_mail.py
new file mode 100644
index 00000000..a40e44a4
--- /dev/null
+++ b/service/pixelated/adapter/welcome_mail.py
@@ -0,0 +1,28 @@
+#
+# 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 <http://www.gnu.org/licenses/>.
+import pkg_resources
+from email import message_from_file
+from pixelated.adapter.model.mail import InputMail
+
+
+def add_welcome_mail(mail_store):
+ welcome_mail = pkg_resources.resource_filename('pixelated.assets', 'welcome.mail')
+
+ with open(welcome_mail) as mail_template_file:
+ mail_template = message_from_file(mail_template_file)
+
+ input_mail = InputMail.from_python_mail(mail_template)
+ mail_store.add_mail('INBOX', input_mail.raw)
diff --git a/service/pixelated/application.py b/service/pixelated/application.py
index aa0db132..73d978da 100644
--- a/service/pixelated/application.py
+++ b/service/pixelated/application.py
@@ -14,37 +14,36 @@
# 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 logging
-from email import message_from_file
-from twisted.internet import reactor
-from twisted.internet import defer
-from twisted.internet import ssl
from OpenSSL import SSL
from OpenSSL import crypto
+from leap.common.events import (server as events_server,
+ register, catalog as events)
+from twisted.cred import portal
+from twisted.cred.checkers import AllowAnonymousAccess
+from twisted.internet import defer
+from twisted.internet import reactor
+from twisted.internet import ssl
-from pixelated.adapter.model.mail import InputMail
+from pixelated.adapter.welcome_mail import add_welcome_mail
from pixelated.config import arguments
-from pixelated.config.services import Services
-from pixelated.config.leap import initialize_leap
from pixelated.config import logger
+from pixelated.config.leap import initialize_leap_single_user, init_monkeypatches, initialize_leap_provider
+from pixelated.config.services import Services
from pixelated.config.site import PixelatedSite
+from pixelated.resources.auth import LeapPasswordChecker, PixelatedRealm, PixelatedAuthSessionWrapper, SessionChecker
+from pixelated.resources.login_resource import LoginResource
from pixelated.resources.root_resource import RootResource
-from leap.common.events import (
- register,
- catalog as events
-)
-
-import logging
-
log = logging.getLogger(__name__)
class ServicesFactory(object):
- def __init__(self):
+ def __init__(self, mode):
self._services_by_user = {}
+ self.mode = mode
def is_logged_in(self, user_id):
return user_id in self._services_by_user
@@ -62,11 +61,28 @@ class ServicesFactory(object):
self._services_by_user[user_id] = services
+class SingleUserServicesFactory(object):
+ def __init__(self, mode):
+ self._services = None
+ self.mode = mode
+
+ def add_session(self, user_id, services):
+ self._services = services
+
+ def services(self, user_id):
+ return self._services
+
+
+class UserAgentMode(object):
+ def __init__(self, is_single_user):
+ self.is_single_user = is_single_user
+
+
@defer.inlineCallbacks
-def start_user_agent(root_resource, services_factory, leap_home, leap_session):
+def start_user_agent_in_single_user_mode(root_resource, services_factory, leap_home, leap_session):
log.info('Bootstrap done, loading services for user %s' % leap_session.user_auth.username)
- services = Services(leap_home, leap_session)
+ services = Services(leap_session)
yield services.setup()
if leap_session.fresh_account:
@@ -77,7 +93,7 @@ def start_user_agent(root_resource, services_factory, leap_home, leap_session):
root_resource.initialize()
# soledad needs lots of threads
- reactor.threadpool.adjustPoolsize(5, 15)
+ reactor.getThreadPool().adjustPoolsize(5, 15)
log.info('Done, the user agent is ready to be used')
@@ -96,27 +112,21 @@ def _ssl_options(sslkey, sslcert):
return options
+def _create_service_factory(args):
+ if args.single_user:
+ return SingleUserServicesFactory(UserAgentMode(is_single_user=True))
+ else:
+ return ServicesFactory(UserAgentMode(is_single_user=False))
+
+
def initialize():
log.info('Starting the Pixelated user agent')
args = arguments.parse_user_agent_args()
logger.init(debug=args.debug)
- services_factory = ServicesFactory()
+ services_factory = _create_service_factory(args)
resource = RootResource(services_factory)
- start_site(args, resource)
-
- deferred = initialize_leap(args.leap_provider_cert,
- args.leap_provider_cert_fingerprint,
- args.credentials_file,
- args.organization_mode,
- args.leap_home)
-
- deferred.addCallback(
- lambda leap_session: start_user_agent(
- resource,
- services_factory,
- args.leap_home,
- leap_session))
+ deferred = _start_mode(args, resource, services_factory)
def _quit_on_error(failure):
failure.printTraceback()
@@ -129,23 +139,66 @@ def initialize():
deferred.addCallback(_register_shutdown_on_token_expire)
deferred.addErrback(_quit_on_error)
+ log.info('Running the reactor')
+
reactor.run()
+def _start_mode(args, resource, services_factory):
+ if services_factory.mode.is_single_user:
+ deferred = _start_in_single_user_mode(args, resource, services_factory)
+ else:
+ deferred = _start_in_multi_user_mode(args, resource, services_factory)
+ return deferred
+
+
+def _start_in_multi_user_mode(args, root_resource, services_factory):
+ if args.provider is None:
+ raise ValueError('provider name is required')
+
+ init_monkeypatches()
+ events_server.ensure_server()
+
+ config, provider = initialize_leap_provider(args.provider, args.leap_provider_cert, args.leap_provider_cert_fingerprint, args.leap_home)
+
+ checker = LeapPasswordChecker(args, provider)
+ session_checker = SessionChecker()
+ anonymous_resource = LoginResource(services_factory)
+
+ realm = PixelatedRealm(root_resource, anonymous_resource)
+ _portal = portal.Portal(realm, [checker, session_checker, AllowAnonymousAccess()])
+
+ protected_resource = PixelatedAuthSessionWrapper(_portal, root_resource, anonymous_resource, [])
+ anonymous_resource.set_portal(_portal)
+
+ start_site(args, protected_resource)
+
+ root_resource.initialize(_portal)
+ reactor.getThreadPool().adjustPoolsize(5, 15)
+
+ return defer.succeed(None)
+
+
+def _start_in_single_user_mode(args, resource, services_factory):
+ start_site(args, resource)
+ deferred = initialize_leap_single_user(args.leap_provider_cert,
+ args.leap_provider_cert_fingerprint,
+ args.credentials_file,
+ args.organization_mode,
+ args.leap_home)
+ deferred.addCallback(
+ lambda leap_session: start_user_agent_in_single_user_mode(
+ resource,
+ services_factory,
+ args.leap_home,
+ leap_session))
+ return deferred
+
+
def start_site(config, resource):
- log.info('Starting the API with the loading screen on port %s' % config.port)
+ log.info('Starting the API on port %s' % config.port)
if config.sslkey and config.sslcert:
reactor.listenSSL(config.port, PixelatedSite(resource), _ssl_options(config.sslkey, config.sslcert),
interface=config.host)
else:
reactor.listenTCP(config.port, PixelatedSite(resource), interface=config.host)
-
-
-def add_welcome_mail(mail_store):
- current_path = os.path.dirname(os.path.abspath(__file__))
- welcome_mail = os.path.join(current_path, 'assets', 'welcome.mail')
- with open(welcome_mail) as mail_template_file:
- mail_template = message_from_file(mail_template_file)
-
- input_mail = InputMail.from_python_mail(mail_template)
- mail_store.add_mail('INBOX', input_mail.raw)
diff --git a/service/pixelated/assets/favicon.png b/service/pixelated/assets/favicon.png
new file mode 100644
index 00000000..e14841c7
--- /dev/null
+++ b/service/pixelated/assets/favicon.png
Binary files differ
diff --git a/service/pixelated/assets/hive-bg.png b/service/pixelated/assets/hive-bg.png
new file mode 100644
index 00000000..77316967
--- /dev/null
+++ b/service/pixelated/assets/hive-bg.png
Binary files differ
diff --git a/service/pixelated/assets/login.html b/service/pixelated/assets/login.html
new file mode 100644
index 00000000..734fffd0
--- /dev/null
+++ b/service/pixelated/assets/login.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Pixelated - Login</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="icon" type="image/png" href="/startup-assets/favicon.png">
+ <link rel="stylesheet" type="text/css" href="/startup-assets/normalize.min.css">
+ <link rel="stylesheet" type="text/css" href="/startup-assets/pixelated.css">
+ <link rel="stylesheet" type="text/css" href="/startup-assets/opensans.css">
+</head>
+<body>
+<div class="content">
+ <div class="login">
+
+ <img class="logo" src="/startup-assets/pixelated-logo-orange.svg" alt="Pixelated logo" />
+
+
+ <form class="standard" id="login_form" action="/login" method="post">
+ <input type="text" name="username" id="email" class="text-field" placeholder=" username" tabindex="1" autofocus>
+ <input type="password" name="password" id="password" class="text-field" placeholder=" password" tabindex="2" autocomplete="off">
+
+ <input type="submit" name="login" value="Login" class="button" tabindex="3">
+
+ </form>
+ </div>
+ <div class="disclaimer">
+ Some disclaimer
+ </div>
+</div>
+</body>
+
+</html>
diff --git a/service/pixelated/assets/normalize.min.css b/service/pixelated/assets/normalize.min.css
new file mode 100644
index 00000000..d3c7f4d5
--- /dev/null
+++ b/service/pixelated/assets/normalize.min.css
@@ -0,0 +1 @@
+/*! normalize.css v3.0.1 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0} \ No newline at end of file
diff --git a/service/pixelated/assets/opensans.css b/service/pixelated/assets/opensans.css
new file mode 100644
index 00000000..a42f346c
--- /dev/null
+++ b/service/pixelated/assets/opensans.css
@@ -0,0 +1,69 @@
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 300;
+ src: local("Open Sans Light"), local("OpenSans-Light"), url("/fonts/OpenSans-Light.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ src: local("Open Sans"), local("OpenSans"), url("/fonts/OpenSans.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 600;
+ src: local("Open Sans Semibold"), local("OpenSans-Semibold"), url("/fonts/OpenSans-Semibold.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 700;
+ src: local("Open Sans Bold"), local("OpenSans-Bold"), url("/fonts/OpenSans-Bold.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 800;
+ src: local("Open Sans Extrabold"), local("OpenSans-Extrabold"), url("/fonts/OpenSans-Extrabold.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 300;
+ src: local("Open Sans Light Italic"), local("OpenSansLight-Italic"), url("/fonts/OpenSansLight-Italic.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 400;
+ src: local("Open Sans Italic"), local("OpenSans-Italic"), url("/fonts/OpenSans-Italic.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 600;
+ src: local("Open Sans Semibold Italic"), local("OpenSans-SemiboldItalic"), url("/fonts/OpenSans-SemiboldItalic.woff ") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 700;
+ src: local("Open Sans Bold Italic"), local("OpenSans-BoldItalic"), url("/fonts/OpenSans-BoldItalic.woff") format("woff");
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 800;
+ src: local("Open Sans Extrabold Italic"), local("OpenSans-ExtraboldItalic"), url("/fonts/OpenSans-ExtraboldItalic.woff") format("woff");
+}
diff --git a/service/pixelated/assets/pixelated-logo-orange.svg b/service/pixelated/assets/pixelated-logo-orange.svg
new file mode 100644
index 00000000..7e0ef43d
--- /dev/null
+++ b/service/pixelated/assets/pixelated-logo-orange.svg
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="509.707px" height="142.439px" viewBox="0 0 509.707 142.439" enable-background="new 0 0 509.707 142.439"
+ xml:space="preserve">
+<g>
+ <path fill="#F9A731" d="M0,35.469v71.365l62.837,35.605l62.833-35.605V35.469L62.813,0L0,35.469z M60.262,116.617L23.735,96.332
+ V52.46l36.586,20.999L60.262,116.617z M101.936,96.332l-36.148,20.285l0.067-43.123l36.081-21.034V96.332z M101.936,46.44
+ L62.951,69.553L23.733,46.44l39.218-21.131L101.936,46.44z"/>
+ <path fill="#F9A731" d="M169.505,42.332h-19.968v59.328h13.52V79.655h6.448c11.579,0,20.279-6.832,20.279-19.056
+ C189.784,48.302,181.084,42.332,169.505,42.332z M166.866,68.868h-3.809v-15.75h3.809c5.323,0,10.357,1.798,10.357,7.91
+ C177.224,67.07,172.189,68.868,166.866,68.868z"/>
+ <rect x="194.309" y="42.332" fill="#F9A731" width="13.52" height="59.328"/>
+ <polygon fill="#F9A731" points="266.516,42.332 249.689,42.332 238.759,58.514 227.827,42.332 211.721,42.332 230.417,69.73
+ 210.228,101.66 226.982,101.66 238.759,81.453 250.534,101.66 268.01,101.66 247.099,69.73 "/>
+ <polygon fill="#F9A731" points="270.128,101.66 304.069,101.66 304.069,89.795 283.647,89.795 283.647,77.857 303.207,77.857
+ 303.207,65.991 283.647,65.991 283.647,54.199 304.069,54.199 304.069,42.332 270.128,42.332 "/>
+ <path fill="#F9A731" d="M354.807,42.332l-19.156,47.463H322.33V42.332h-13.52v59.328h22.053h11.888h2.636l4.386-11.865h22.578
+ l4.391,11.865h14.524l-23.944-59.328H354.807z M354.377,77.928l6.614-17.257h0.145l6.615,17.257H354.377z"/>
+ <polygon fill="#F9A731" points="379.939,54.199 394.073,54.199 394.073,101.66 407.592,101.66 407.592,54.199 421.687,54.199
+ 421.687,42.332 379.939,42.332 "/>
+ <polygon fill="#F9A731" points="426.265,101.66 460.207,101.66 460.207,89.795 439.785,89.795 439.785,77.857 459.344,77.857
+ 459.344,65.991 439.785,65.991 439.785,54.199 460.207,54.199 460.207,42.332 426.265,42.332 "/>
+ <path fill="#F9A731" d="M479.792,42.332h-14.94v59.328h14.94c16.324,0,29.914-12.37,29.914-29.699
+ C509.707,54.701,496.044,42.332,479.792,42.332z M480.457,89.577h-2.084V54.414h2.084c10.067,0,16.9,7.695,16.9,17.619
+ C497.285,81.955,490.455,89.577,480.457,89.577z"/>
+</g>
+</svg>
diff --git a/service/pixelated/assets/pixelated.css b/service/pixelated/assets/pixelated.css
new file mode 100644
index 00000000..b435b828
--- /dev/null
+++ b/service/pixelated/assets/pixelated.css
@@ -0,0 +1,127 @@
+body {
+ font-family: "Open Sans", "Microsoft YaHei", "Hiragino Sans GB", "Hiragino Sans GB W3", "微软雅黑", "Helvetica Neue", Arial, sans-serif;
+ background-color: #EAEAEA;
+ height: 100vh;
+ color: #3E3A37;
+
+ background-image: url("hive-bg.png");
+ background-repeat: repeat;
+}
+
+.content {
+ height: 100vh;
+ width: 100%;
+}
+
+.error {
+ color: #D72A25;
+ margin: 10px 0 0 0;
+}
+
+.message-panel {
+ width: 100%;
+ margin: 10px auto;
+ z-index: 10000;
+ text-align: center;
+ }
+
+.message-panel span{
+ background: #F7E8AF;
+ padding: 5px 60px;
+ border: 1px solid #f2db81;
+ color: #987b0f;
+ box-shadow: 1px 1px 3px #69560b;
+}
+
+.message-panel.message-panel-small span{
+ padding: 5px 0px;
+ display: inline-block;
+ width: 100%;
+}
+
+.login {
+ display: block;
+ width: 240px;
+ margin: auto;
+ padding: 45px 40px 35px 40px;
+ background-color: #FFF;
+ margin-top: 2%;
+ margin-bottom: 2%;
+}
+
+form#login_form {
+ padding: 10px 0;
+}
+
+.disclaimer {
+ display: block;
+ margin-top: 10%;
+ width: 50%;
+ margin: auto;
+ background-color: #2BA6CB;
+ color: #FFFFFF;
+ font-weight: 300;
+ font-size: 0.8rem;
+ padding: 1em;
+ margin-bottom: 20px;
+}
+
+.disclaimer li {
+ margin-top: 1em;
+}
+
+.logo {
+ width: 100%;
+ height: auto;
+}
+
+input {
+ display: block;
+ margin: 10px 0;
+}
+
+input.text-field {
+ width: 97%;
+}
+
+button, .button, input[type=button] {
+ cursor: pointer;
+ margin: 0 0 1.25rem;
+ border: none;
+ position: relative;
+ text-decoration: none;
+ text-align: center;
+ -webkit-appearance: none;
+ display: inline-block;
+ padding: 0.4rem 1.1rem;
+ font-size: 0.9rem;
+ background-color: #2ba6cb;
+ border-color: #2285a2;
+ color: white;
+ -webkit-border-radius: 2px;
+ -moz-border-radius: 2px;
+ -ms-border-radius: 2px;
+ -o-border-radius: 2px;
+ border-radius: 2px;
+}
+
+button:hover, button:focus, .button:hover, .button:focus, input[type=button]:hover, input[type=button]:focus {
+ background-color: #2285a2;
+ outline: none;
+ color: white;
+}
+
+ul.accounts {
+ margin-bottom: 5%;
+}
+
+ul.accounts li {
+ display: inline-block;
+ list-style: none;
+ margin-right: 35px;
+ margin-top: 0px;
+}
+
+ul.accounts li span {
+ font-weight: bold;
+}
diff --git a/service/pixelated/config/arguments.py b/service/pixelated/config/arguments.py
index 87484b9b..d8a18c16 100644
--- a/service/pixelated/config/arguments.py
+++ b/service/pixelated/config/arguments.py
@@ -28,8 +28,11 @@ def parse_user_agent_args():
parser.add_argument('--port', type=int, default=3333, help='the port to run the user agent on')
parser.add_argument('-sk', '--sslkey', metavar='<server.key>', default=None, help='use specified file as web server\'s SSL key (when using the user-agent together with the pixelated-dispatcher)')
parser.add_argument('-sc', '--sslcert', metavar='<server.crt>', default=None, help='use specified file as web server\'s SSL certificate (when using the user-agent together with the pixelated-dispatcher)')
+ parser.add_argument('--multi-user', help='Run user agent in multi user mode', action='store_false', default=True, dest='single_user')
+ parser.add_argument('-p', '--provider', help='specify a provider for mutli-user mode', metavar='<provider host>', default=None, dest='provider')
args = parser.parse_args()
+
return args
diff --git a/service/pixelated/config/credentials.py b/service/pixelated/config/credentials.py
index ae1bc4f3..a6da86e6 100644
--- a/service/pixelated/config/credentials.py
+++ b/service/pixelated/config/credentials.py
@@ -50,5 +50,4 @@ def read_from_file(credentials_file):
def read_from_dispatcher():
config = json.loads(sys.stdin.read())
-
return config['leap_provider_hostname'], config['user'], config['password']
diff --git a/service/pixelated/config/leap.py b/service/pixelated/config/leap.py
index 00723224..b8e5f50d 100644
--- a/service/pixelated/config/leap.py
+++ b/service/pixelated/config/leap.py
@@ -10,28 +10,61 @@ from twisted.internet import defer
import os
import logging
+import logging
+log = logging.getLogger(__name__)
+
+
+def initialize_leap_provider(provider_hostname, provider_cert, provider_fingerprint, leap_home):
+ LeapCertificate.set_cert_and_fingerprint(provider_cert,
+ provider_fingerprint)
+
+ config = LeapConfig(leap_home=leap_home, start_background_jobs=True)
+ provider = LeapProvider(provider_hostname, config)
+ LeapCertificate(provider).setup_ca_bundle()
+
+ return config, provider
+
@defer.inlineCallbacks
-def initialize_leap(leap_provider_cert,
- leap_provider_cert_fingerprint,
- credentials_file,
- organization_mode,
- leap_home,
- initial_sync=True):
+def initialize_leap_multi_user(provider_hostname,
+ leap_provider_cert,
+ leap_provider_cert_fingerprint,
+ credentials_file,
+ organization_mode,
+ leap_home):
+
+ config, provider = initialize_leap_provider(provider_hostname, leap_provider_cert, leap_provider_cert_fingerprint, leap_home)
+
+ defer.returnValue((config, provider))
+
+
+@defer.inlineCallbacks
+def authenticate_user(provider, username, password, initial_sync=True):
+ leap_session = LeapSessionFactory(provider).create(username, password)
+
+ if initial_sync:
+ yield leap_session.initial_sync()
+
+ defer.returnValue(leap_session)
+
+
+@defer.inlineCallbacks
+def initialize_leap_single_user(leap_provider_cert,
+ leap_provider_cert_fingerprint,
+ credentials_file,
+ organization_mode,
+ leap_home,
+ initial_sync=True):
+
init_monkeypatches()
events_server.ensure_server()
+
provider, username, password = credentials.read(organization_mode,
credentials_file)
- LeapCertificate.set_cert_and_fingerprint(leap_provider_cert,
- leap_provider_cert_fingerprint)
- config = LeapConfig(leap_home=leap_home, start_background_jobs=True)
- provider = LeapProvider(provider, config)
- LeapCertificate(provider).setup_ca_bundle()
- leap_session = LeapSessionFactory(provider).create(username, password)
+ config, provider = initialize_leap_provider(provider, leap_provider_cert, leap_provider_cert_fingerprint, leap_home)
- if initial_sync:
- leap_session = yield leap_session.initial_sync()
+ leap_session = yield authenticate_user(provider, username, password)
defer.returnValue(leap_session)
diff --git a/service/pixelated/config/services.py b/service/pixelated/config/services.py
index 1d5d951a..3f254571 100644
--- a/service/pixelated/config/services.py
+++ b/service/pixelated/config/services.py
@@ -18,8 +18,8 @@ logger = logging.getLogger(__name__)
class Services(object):
- def __init__(self, leap_home, leap_session):
- self._leap_home = leap_home
+ def __init__(self, leap_session):
+ self._leap_home = leap_session.config.leap_home
self._leap_session = leap_session
@defer.inlineCallbacks
diff --git a/service/pixelated/maintenance.py b/service/pixelated/maintenance.py
index 4562717f..5e1edac3 100644
--- a/service/pixelated/maintenance.py
+++ b/service/pixelated/maintenance.py
@@ -20,7 +20,7 @@ import random
from twisted.internet import reactor, defer
from twisted.internet.threads import deferToThread
from pixelated.adapter.mailstore.maintenance import SoledadMaintenance
-from pixelated.config.leap import initialize_leap
+from pixelated.config.leap import initialize_leap_single_user
from pixelated.config import logger, arguments
import logging
@@ -39,7 +39,7 @@ def initialize():
@defer.inlineCallbacks
def _run():
- leap_session = yield initialize_leap(
+ leap_session = yield initialize_leap_single_user(
args.leap_provider_cert,
args.leap_provider_cert_fingerprint,
args.credentials_file,
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)