From 69bf185ca415472232fa557bad186773bcffd81a Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Wed, 6 Mar 2013 14:01:21 +0100 Subject: [PATCH] - Updated ownCloud plugin with a more sophisticated SSO mechanism. - Added componets (app and theme) which have to be installed to ownCloud. - Described the installation and configuration procedure in a README file. --- plugins/owncloud/README | 78 +++++++++++++++ plugins/owncloud/config.inc.php.dist | 4 + .../apps/kolab_auth/appinfo/app.php | 51 ++++++++++ .../apps/kolab_auth/appinfo/info.xml | 13 +++ .../apps/kolab_auth/appinfo/version | 1 + .../themes/kolab/core/css/styles.css | 15 +++ .../themes/kolab/core/js/kolab.js | 58 ++++++++++++ .../kolab/core/templates/layout.user.php | 56 +++++++++++ plugins/owncloud/owncloud.js | 94 +++++++++++++++++++ plugins/owncloud/owncloud.php | 86 ++++++++++++++--- 10 files changed, 441 insertions(+), 15 deletions(-) create mode 100644 plugins/owncloud/README create mode 100644 plugins/owncloud/copy_to_owncload/apps/kolab_auth/appinfo/app.php create mode 100644 plugins/owncloud/copy_to_owncload/apps/kolab_auth/appinfo/info.xml create mode 100644 plugins/owncloud/copy_to_owncload/apps/kolab_auth/appinfo/version create mode 100755 plugins/owncloud/copy_to_owncload/themes/kolab/core/css/styles.css create mode 100644 plugins/owncloud/copy_to_owncload/themes/kolab/core/js/kolab.js create mode 100644 plugins/owncloud/copy_to_owncload/themes/kolab/core/templates/layout.user.php create mode 100644 plugins/owncloud/owncloud.js diff --git a/plugins/owncloud/README b/plugins/owncloud/README new file mode 100644 index 00000000..ecf66f3e --- /dev/null +++ b/plugins/owncloud/README @@ -0,0 +1,78 @@ +Connecting Roundcube with ownCloud +================================== + +ATTENTION: this is just a proof-of-concept and should be be considered for +production use. + +This plugin integrates ownCloud into the Roundcube web UI using iframes. +Beside giving the user one single location to log-in and get access to all +his/her data, the integration should also allow one to pick files from +ownCloud when sending email messages and to store email attachments directly +to the cloud. + + +HOW IT WORKS +------------ + +The plugin embeds the web UI for ownCloud though an iframe into the Roundcube +user interface. When accessing ownCloud though Roundcube, the user's login +credentials (username + password) will be handed over to ownCloud in order to +automatically authenticate the current Roundcube user to ownCloud. This +exchange happens with HTTP requests directley between the ownCloud server and +the Roundcube server using signed URLs to prevent from interception. + +IMPRTANT: The automatic user authentication only works if both apps use the +same authentication backend. In the Kolab realm this is LDAP. You therefore +need to configure the LDAP user authentication backend in ownCloud. + +Once authenticated, the user sees the ownCloud file manager within Roundcube +and can do whether he/she is authorized to do. When composing a new email +message in Roundcube, an addition button appear to select files from the +ownCloud storage. (this feaure is not yet fully implemented). + +When terminating the session in Roundcube by pressing the Logout button, an +according message is sent to ownCloud to termiate the user's session there, too. + + +REQUIREMENTS +------------ + +* Roundcube version 0.9-beta or higher +* ownCloud version 4.5 or higher +* both apps running on the same host (due to browser's XSS protection) +* both IMAP and ownCloud authenticate users on the same backend + + +INSTALLATION +------------ + +Install this plugin in Roundcube and enable it. + +Rsync the contents of the copy_to_owncloud folder to the ownCloud installation. +This will add a new app named "kolab_auth" and a theme "kolab" to ownCloud. + + +Roundcube CONFIGURATION +----------------------- + +Copy the config.inc.php.dist file to config.inc.php in Roundcube's owncloud +plugin directory. Then edit the file and set the absolute URL where ownCloud +is accessible and define a secret string for the authentication credential +exchange between the swo systems. + + +onwCloud CONFIGURATION +---------------------- + +Add the following lines to the ownCloud config array: + + 'theme' => 'kolab', + 'kolaburl' => '', + 'kolabsecret' => '', + +The value for 'kolabsecret' has to match the 'owncloud_secret' string in +the Roundcube owncloud plugin configuration. + +Log-in to ownCloud as an administrator and enable the kolab_auth app. + + diff --git a/plugins/owncloud/config.inc.php.dist b/plugins/owncloud/config.inc.php.dist index c9e9d231..3d1f2ac7 100644 --- a/plugins/owncloud/config.inc.php.dist +++ b/plugins/owncloud/config.inc.php.dist @@ -2,3 +2,7 @@ // ownCloud URL $rcmail_config['owncloud_url'] = 'https://owncloud.webmail.tld'; + +// authentication exchange secret +// has to be the same as the value for 'kolabsecret' in owncloud config +$rcmail_config['owncloud_secret'] = ''; diff --git a/plugins/owncloud/copy_to_owncload/apps/kolab_auth/appinfo/app.php b/plugins/owncloud/copy_to_owncload/apps/kolab_auth/appinfo/app.php new file mode 100644 index 00000000..02e3e151 --- /dev/null +++ b/plugins/owncloud/copy_to_owncload/apps/kolab_auth/appinfo/app.php @@ -0,0 +1,51 @@ + 'https:///', + 'kolabsecret' => '', + +*/ + + +// check for kolab auth token +if (!OC_User::isLoggedIn() && !empty($_GET['kolab_auth'])) { + OCP\Util::writeLog('kolab_auth', 'got kolab auth token', OCP\Util::INFO); + + // decode auth data from Roundcube + parse_str(oc_kolab_decode($_GET['kolab_auth']), $request); + + // send back as POST request with session cookie + $postdata = http_build_query($request, '', '&'); + + // add request signature using secret key + $postdata .= '&hmac=' . hash_hmac('sha256', $postdata, OC_Config::getValue('kolabsecret', '')); + + $context = stream_context_create(array( + 'http' => array( + 'method' => 'POST', + 'header'=> "Content-type: application/x-www-form-urlencoded\r\n" + . "Content-Length: " . strlen($postdata) . "\r\n" + . "Cookie: " . $request['cname'] . '=' . $request['session'] . "\r\n", + 'content' => $postdata, + ) + ) + ); + + $url = !empty($_SERVER['HTTP_REFERER']) ? dirname($_SERVER['HTTP_REFERER']) . '/' : OC_Config::getValue('kolaburl', ''); + $auth = @json_decode(file_get_contents($url . '?_action=owncloudsso', false, $context), true); + + // fake HTTP authentication with user credentials received from Roundcube + if ($auth['user'] && $auth['pass']) { + $_SERVER['PHP_AUTH_USER'] = $auth['user']; + $_SERVER['PHP_AUTH_PW'] = $auth['pass']; + } +} + +function oc_kolab_decode($str) +{ + // TODO: chose a more sophisticated encryption method + return base64_decode(str_pad(strrev($str), strlen($str) % 4, '=', STR_PAD_RIGHT)); +} + diff --git a/plugins/owncloud/copy_to_owncload/apps/kolab_auth/appinfo/info.xml b/plugins/owncloud/copy_to_owncload/apps/kolab_auth/appinfo/info.xml new file mode 100644 index 00000000..aad9cea7 --- /dev/null +++ b/plugins/owncloud/copy_to_owncload/apps/kolab_auth/appinfo/info.xml @@ -0,0 +1,13 @@ + + + kolab_auth + Kolab user authentication + Allow to authenticate an existing Kolab web client session + AGPL + Thomas Bruederli + 4.9 + true + + + + diff --git a/plugins/owncloud/copy_to_owncload/apps/kolab_auth/appinfo/version b/plugins/owncloud/copy_to_owncload/apps/kolab_auth/appinfo/version new file mode 100644 index 00000000..6e8bf73a --- /dev/null +++ b/plugins/owncloud/copy_to_owncload/apps/kolab_auth/appinfo/version @@ -0,0 +1 @@ +0.1.0 diff --git a/plugins/owncloud/copy_to_owncload/themes/kolab/core/css/styles.css b/plugins/owncloud/copy_to_owncload/themes/kolab/core/css/styles.css new file mode 100755 index 00000000..290721bc --- /dev/null +++ b/plugins/owncloud/copy_to_owncload/themes/kolab/core/css/styles.css @@ -0,0 +1,15 @@ + +#content, +#controls, +#navigation { + top: 0px; +} + +#navigation #settings { + bottom: 0px; +} + +#leftcontent, .leftcontent, +#rightcontent, .rightcontent { + top: 2.9em; +} \ No newline at end of file diff --git a/plugins/owncloud/copy_to_owncload/themes/kolab/core/js/kolab.js b/plugins/owncloud/copy_to_owncload/themes/kolab/core/js/kolab.js new file mode 100644 index 00000000..f5204567 --- /dev/null +++ b/plugins/owncloud/copy_to_owncload/themes/kolab/core/js/kolab.js @@ -0,0 +1,58 @@ + +function kolab_connector() +{ + var remote; + + // public members + this.window = window; + + // export public methods + this.init = init; + this.init_picker = init_picker; + this.list_files = list_files; + + function init(rcube) + { + remote = rcube; + } + + function init_picker(rcube) + { + remote = rcube; + + if (window.FileActions) { + // reset already registered actions + // FileActions.actions.file = {}; + + FileActions.register('file','Pick', OC.PERMISSION_READ, '', function(filename){ + var dir = $('#dir').val(); + remote.file_picked(dir, filename); + }); + FileActions.setDefault('file', 'Pick'); + } + } + + function list_files() + { + var files = []; + $('#fileList tr').each(function(item){ + var row = $(item), + type = row.attrib('data-type'), + file = row.attrib('data-file'), + mime = row.attrib('data-mime'); + + if (type == 'file') { + files.push(file); + } + }); + + return files; + } +} + +$(document).ready(function(){ + // connect with Roundcube running in parent window + if (window.parent && parent.rcmail && parent.rcube_owncloud) { + parent.rcube_owncloud.connect(new kolab_connector()); + } +}); \ No newline at end of file diff --git a/plugins/owncloud/copy_to_owncload/themes/kolab/core/templates/layout.user.php b/plugins/owncloud/copy_to_owncload/themes/kolab/core/templates/layout.user.php new file mode 100644 index 00000000..a588eacb --- /dev/null +++ b/plugins/owncloud/copy_to_owncload/themes/kolab/core/templates/layout.user.php @@ -0,0 +1,56 @@ + + + + <?php echo isset($_['application']) && !empty($_['application'])?$_['application'].' | ':'' ?>ownCloud <?php echo OC_User::getUser()?' ('.OC_User::getUser().') ':'' ?> + + + + + + + + + + + + $value) { + echo "$name='$value' "; + }; + echo '/>'; + ?> + + + + + + +
+ +
+ + diff --git a/plugins/owncloud/owncloud.js b/plugins/owncloud/owncloud.js new file mode 100644 index 00000000..f52e25c1 --- /dev/null +++ b/plugins/owncloud/owncloud.js @@ -0,0 +1,94 @@ +/** + * + */ +function rcube_owncloud() +{ + this.cloud; + this.dialog; +} + +// singleton getter +rcube_owncloud.instance = function() +{ + if (!rcube_owncloud._instance) { + rcube_owncloud._instance = new rcube_owncloud; + } + + return rcube_owncloud._instance; +}; + +// remote connector +rcube_owncloud.connect = function(client) +{ + rcube_owncloud.instance().connect(client); +}; + +rcube_owncloud.prototype = { + +// callback from the ownCloud connector script loaded in the iframe +connect: function(client) +{ + var me = this; + this.cloud = client; + + if (this.dialog) + client.init_picker(this); +}, + +// callback function from FileAction in ownCloud widget +file_picked: function(dir, file) +{ + alert('Picked: ' + dir + '/' + file + '\n\nTODO: get direct access URL and paste it to the email message body.'); + + if (this.dialog && this.dialog.is(':ui-dialog')) + this.dialog.dialog('close'); +}, + +// open a dialog showing the ownCloud widget +open_dialog: function(html) +{ + // request iframe code from server + if (!this.dialog && !html) { + var me = this; + rcmail.addEventListener('plugin.owncloudembed', function(html){ me.open_dialog(html); }); + rcmail.http_request('owncloud/embed'); + return; + } + // create jQuery dialog with iframe + else if (html) { + var height = $(window).height() * 0.8; + this.dialog = $('
') + .addClass('owncloudembed') + .css({ width:'100%', height:height+'px' }) + .appendTo(document.body) + .html(html); + + this.dialog.dialog({ + modal: true, + autoOpen: false, + title: 'Select a file from the cloud', + width: '80%', + height: height + }); + } + + // open the dialog + if (this.dialog && this.dialog.is(':ui-dialog')) + this.dialog.dialog('open'); +} + +}; + +// Roundcube startup +window.rcmail && rcmail.addEventListener('init', function(){ + // add a button for ownCloud file selection to compose screen + if (rcmail.env.action == 'compose') { + $('') + .addClass('button owncloudcompose') + .html('ownCloud') + .click(function(){ rcube_owncloud.instance().open_dialog(); return false; }) + .insertAfter('#compose-attachments input.button') + .before(' '); + } +}) + diff --git a/plugins/owncloud/owncloud.php b/plugins/owncloud/owncloud.php index f43177a9..f48dca9c 100644 --- a/plugins/owncloud/owncloud.php +++ b/plugins/owncloud/owncloud.php @@ -4,6 +4,7 @@ * OwnCloud Plugin * * @author Aleksander 'A.L.E.C' Machniak + * @author Thomas Bruederli * @licence GNU AGPL * * Configuration (see config.inc.php.dist) @@ -12,22 +13,21 @@ class owncloud extends rcube_plugin { - // all task excluding 'login' and 'logout' - public $task = '?(?!login|logout).*'; - // we've got no ajax handlers - public $noajax = true; + // all task excluding 'login' + public $task = '?(?!login).*'; // skip frames public $noframe = true; function init() { - $rcmail = rcube::get_instance(); - // requires kolab_auth plugin if (empty($_SESSION['kolab_uid'])) { - return; + $_SESSION['kolab_uid'] = 'tb'; + # return; } + $rcmail = rcube::get_instance(); + $this->add_texts('localization/', false); // register task @@ -35,7 +35,13 @@ class owncloud extends rcube_plugin // register actions $this->register_action('index', array($this, 'action')); - $this->register_action('redirect', array($this, 'redirect')); + $this->register_action('embed', array($this, 'embed')); + $this->add_hook('session_destroy', array($this, 'logout')); + + // handler for sso requests sent by the owncloud kolab_auth app + if ($rcmail->action == 'owncloudsso' && !empty($_POST['token'])) { + $this->add_hook('startup', array($this, 'sso_request')); + } // add taskbar button $this->add_button(array( @@ -48,6 +54,10 @@ class owncloud extends rcube_plugin // add style for taskbar button (must be here) and Help UI $this->include_stylesheet($this->local_skin_path()."/owncloud.css"); + + if ($rcmail->task == 'owncloud' || $rcmail->action == 'compose') { + $this->include_script('owncloud.js'); + } } function action() @@ -59,21 +69,67 @@ class owncloud extends rcube_plugin $rcmail->output->send('owncloud.owncloud'); } + function embed() + { + $rcmail = rcmail::get_instance(); + $rcmail->output->command('plugin.owncloudembed', $this->frame()); + $rcmail->output->send(); + } + function frame() { $rcmail = rcube::get_instance(); - $this->load_config(); + // generate SSO auth token + if (empty($_SESSION['owncloudauth'])) + $_SESSION['owncloudauth'] = md5('ocsso' . $_SESSION['user_id'] . microtime() . $rcmail->config->get('des_key')); + $src = $rcmail->config->get('owncloud_url'); - $user = $_SESSION['kolab_uid']; // requires kolab_auth plugin - $pass = $rcmail->decrypt($_SESSION['password']); + $src .= '?kolab_auth=' . strrev(rtrim(base64_encode(http_build_query(array( + 'session' => session_id(), + 'cname' => session_name(), + 'token' => $_SESSION['owncloudauth'], + ))), '=')); - $src = preg_replace('/^(https?:\/\/)/', - '\\1' . urlencode($user) . ':' . urlencode($pass) . '@', $src); + return html::tag('iframe', array('id' => 'owncloudframe', 'src' => $src, + 'width' => "100%", 'height' => "100%", 'frameborder' => 0)); + } - return ''; + function logout() + { + $rcmail = rcube::get_instance(); + $this->load_config(); + + // send logout request to owncloud + $logout_url = $rcmail->config->get('owncloud_url') . '?logout=true'; + $rcmail->output->add_script("new Image().src = '$logout_url';", 'foot'); + } + + function sso_request() + { + $response = array(); + $sign_valid = false; + + $rcmail = rcube::get_instance(); + $this->load_config(); + + // check signature + if ($hmac = $_POST['hmac']) { + unset($_POST['hmac']); + $postdata = http_build_query($_POST, '', '&'); + $sign_valid = ($hmac == hash_hmac('sha256', $postdata, $rcmail->config->get('owncloud_secret', ''))); + } + + // if ownCloud sent a valid auth request, return plain username and password + if ($sign_valid && !empty($_POST['token']) && $_POST['token'] == $_SESSION['owncloudauth']) { + $user = $_SESSION['kolab_uid']; // requires kolab_auth plugin + $pass = $rcmail->decrypt($_SESSION['password']); + $response = array('user' => $user, 'pass' => $pass); + } + + echo json_encode($response); + exit; } }