diff --git a/plugins/kolab_2fa/config.inc.php.dist b/plugins/kolab_2fa/config.inc.php.dist index 003e5826..a601823b 100644 --- a/plugins/kolab_2fa/config.inc.php.dist +++ b/plugins/kolab_2fa/config.inc.php.dist @@ -1,82 +1,124 @@ * * This program 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. * * This program 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 this program. If not, see . */ // available methods/providers. Supported methods are: 'totp','hotp','yubikey' $config['kolab_2fa_drivers'] = array('totp'); // backend for storing 2-factor-auth related per-user settings // available backends are: 'roundcube', 'ldap', 'sql' $config['kolab_2fa_storage'] = 'roundcube'; // additional config options for the above storage backend // here an example for the LDAP backend: $config['kolab_2fa_storage_config'] = array( + 'debug' => false, 'hosts' => array('localhost'), 'port' => 389, - 'bind_dn' => 'uid=kolab-service,ou=Special Users,dc=example,dc=org', + 'bind_dn' => 'uid=kolab-auth-service,ou=Special Users,dc=example,dc=org', 'bind_pass' => 'Welcome2KolabSystems', - 'base_dn' => 'ou=People,dc=example,dc=org', - 'filter' => '(&(objectClass=inetOrgPerson)(mail=%fu))', + 'base_dn' => 'ou=Tokens,dc=example,dc=org', + // filter used to list stored factors for a user + 'filter' => '(&(objectClass=ipaToken)(objectclass=ldapSubEntry)(ipatokenOwner=%fu))', 'scope' => 'sub', - 'debug' => true, + // translates driver properties to LDAP attributes 'fieldmap' => array( - 'active' => 'nsroledn', - '@totp' => 'kolabAuthTOTP', - '@hotp' => 'kolabAuthHOTP', - '@yubikey' => 'kolabAuthYubikey', + 'label' => 'cn', + 'id' => 'ipatokenUniqueID', + 'active' => 'ipatokenDisabled', + 'created' => 'ipatokenNotBefore', + 'userdn' => 'ipatokenOwner', + 'secret' => 'ipatokenOTPkey', + // HOTP attributes + 'counter' => 'ipatokenHOTPcounter', + 'digest' => 'ipatokenOTPalgorithm', + 'digits' => 'ipatokenOTPdigits', ), + // LDAP object classes derived from factor IDs (prefix) + // will be translated into the %c placeholder + 'classmap' => array( + 'totp:' => 'ipatokenTOTP', + 'hotp:' => 'ipatokenHOTP', + '*' => 'ipaToken', + ), + // translates property values into LDAP attribute values and vice versa 'valuemap' => array( - 'nsroledn' => array( - 'totp' => 'cn=totp-user,dc=example,dc=org', - 'hotp' => 'cn=hotp-user,dc=example,dc=org', - 'yubikey' => 'cn=yubikey-user,dc=example,dc=org', + 'active' => array( + false => 'TRUE', + true => 'FALSE', ), ), + // specify non-string data types for properties for implicit conversion + 'attrtypes' => array( + 'created' => 'datetime', + 'counter' => 'integer', + 'digits' => 'integer', + ), + // apply these default values to factor records if not specified by the drivers + 'defaults' => array( + 'active' => false, + // these are required for ipatokenHOTP records and should match the kolab_2fa_hotp parameters + 'digest' => 'sha1', + 'digits' => 6, + ), + // use this LDAP attribute to compose DN values for factor entries + 'rdn' => 'ipatokenUniqueID', + // assign these object classes to new factor entries + 'objectclass' => array( + 'top', + 'ipaToken', + '%c', + 'ldapSubEntry', + ), + // add these roles to the user's LDAP record if key prefix-matches a factor entry + 'user_roles' => array( + 'totp:' => 'cn=totp-user,dc=example,dc=org', + 'hotp:' => 'cn=hotp-user,dc=example,dc=org', + ), ); -// force a two-factor authentication for all users +// force a lookup for active authentication factors for this user. // to be set by another plugin (e.g. kolab_auth based on LDAP roles) -// $config['kolab_2fa_factors'] = array('totp'); +// $config['kolab_2fa_check'] = true; // timeout for 2nd factor auth submission (in seconds) $config['kolab_2fa_timeout'] = 60; // configuration parameters for TOTP (uncomment to adjust) $config['kolab_2fa_totp'] = array( // 'digits' => 6, // 'interval' => 30, // 'digest' => 'sha1', // 'issuer' => 'Roundcube', ); // configuration parameters for HOTP (uncomment to adjust) $config['kolab_2fa_hotp'] = array( // 'digits' => 6, // 'window' => 4, // 'digest' => 'sha1', ); // configuration parameters for Yubikey (uncomment to adjust) $config['kolab_2fa_yubikey'] = array( 'clientid' => '123456', 'apikey' => '', // 'hosts' => array('api.myhost1.com','api2.myhost.com'), ); \ No newline at end of file diff --git a/plugins/kolab_2fa/kolab_2fa.php b/plugins/kolab_2fa/kolab_2fa.php index 4ee7e8d8..0d33bf2d 100644 --- a/plugins/kolab_2fa/kolab_2fa.php +++ b/plugins/kolab_2fa/kolab_2fa.php @@ -1,746 +1,751 @@ * * Copyright (C) 2015, Kolab Systems AG * * This program 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. * * This program 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 this program. If not, see . */ class kolab_2fa extends rcube_plugin { public $task = '(login|settings)'; protected $login_verified = null; protected $login_factors = array(); protected $drivers = array(); protected $storage; /** * Plugin init */ public function init() { $this->load_config(); $this->add_hook('startup', array($this, 'startup')); } /** * Startup hook */ public function startup($args) { $rcmail = rcmail::get_instance(); // register library namespace to autoloader $loader = include(INSTALL_PATH . 'vendor/autoload.php'); $loader->set('Kolab2FA', array($this->home . '/lib')); if ($args['task'] === 'login' && $this->api->output) { $this->add_texts('localization/', false); $this->add_hook('authenticate', array($this, 'authenticate')); // process 2nd factor auth step after regular login if ($args['action'] === 'plugin.kolab-2fa-login' /* || !empty($_SESSION['kolab_2fa_factors']) */) { return $this->login_verify($args); } } else if ($args['task'] === 'settings') { $this->add_texts('localization/', !$this->api->output->ajax_call); $this->add_hook('settings_actions', array($this, 'settings_actions')); $this->register_action('plugin.kolab-2fa', array($this, 'settings_view')); $this->register_action('plugin.kolab-2fa-data', array($this, 'settings_data')); $this->register_action('plugin.kolab-2fa-save', array($this, 'settings_save')); $this->register_action('plugin.kolab-2fa-verify', array($this, 'settings_verify')); } return $args; } /** * Handler for 'authenticate' plugin hook. * * ATTENTION: needs to be called *after* kolab_auth::authenticate() */ public function authenticate($args) { // nothing to be done for me if ($args['abort'] || $this->login_verified !== null) { return $args; } $rcmail = rcmail::get_instance(); // parse $host URL $a_host = parse_url($args['host']); $hostname = $_SESSION['hostname'] = $a_host['host'] ?: $args['host']; // 1. find user record (and its prefs) before IMAP login if ($user = rcube_user::query($args['user'], $hostname)) { $rcmail->config->set_user_prefs($user->get_prefs()); } // 2a. let plugins provide the list of active authentication factors - $lookup = $rcmail->plugins->exec_hook('kolab_2fa_factors', array( - 'user' => $args['user'], - 'host' => $hostname, - 'active' => $rcmail->config->get('kolab_2fa_factors'), + $lookup = $rcmail->plugins->exec_hook('kolab_2fa_lookup', array( + 'user' => $args['user'], + 'host' => $hostname, + 'factors' => $rcmail->config->get('kolab_2fa_factors'), + 'check' => $rcmail->config->get('kolab_2fa_check', true), )); - if (isset($lookup['active'])) { - $factors = (array)$lookup['active']; + if (isset($lookup['factors'])) { + $factors = (array)$lookup['factors']; } // 2b. check storage if this user has 2FA enabled - else if ($storage = $this->get_storage($args['user'])) { + else if ($lookup['check'] !== false && ($storage = $this->get_storage($args['user']))) { $factors = (array)$storage->enumerate(); } if (count($factors) > 0) { $args['abort'] = true; $factors = array_unique($factors); // 3. flag session for 2nd factor verification $_SESSION['kolab_2fa_time'] = time(); $_SESSION['kolab_2fa_nonce'] = bin2hex(openssl_random_pseudo_bytes(32)); $_SESSION['kolab_2fa_factors'] = $factors; $_SESSION['username'] = $args['user']; $_SESSION['host'] = $args['host']; $_SESSION['password'] = $rcmail->encrypt($args['pass']); // 4. render to 2nd auth step $this->login_step($factors); } return $args; } /** * Handler for the additional login step requesting the 2FA verification code */ public function login_step($factors) { // replace handler for login form $this->login_factors = array_values($factors); $this->api->output->add_handler('loginform', array($this, 'auth_form')); // focus the code input field on load $this->api->output->add_script('$("input.kolab2facode").first().select();', 'docready'); $this->api->output->send('login'); } /** * Process the 2nd factor code verification form submission */ public function login_verify($args) { $rcmail = rcmail::get_instance(); $time = $_SESSION['kolab_2fa_time']; $nonce = $_SESSION['kolab_2fa_nonce']; $factors = (array)$_SESSION['kolab_2fa_factors']; $this->login_verified = false; $expired = $time < time() - $rcmail->config->get('kolab_2fa_timeout', 120); if (!empty($factors) && !empty($nonce) && !$expired) { // TODO: check signature // try to verify each configured factor foreach ($factors as $factor) { list($method) = explode(':', $factor, 2); // verify the submitted code $code = rcube_utils::get_input_value("_${nonce}_${method}", rcube_utils::INPUT_POST); $this->login_verified = $this->verify_factor_auth($factor, $code); // accept first successful method if ($this->login_verified) { break; } } } if ($this->login_verified) { // restore POST data from session $_POST['_user'] = $_SESSION['username']; $_POST['_host'] = $_SESSION['host']; $_POST['_pass'] = $rcmail->decrypt($_SESSION['password']); } // proceed with regular login ... $args['action'] = 'login'; // session data will be reset in index.php thus additional // auth attempts with intercepted data will be rejected // $rcmail->kill_session(); // we can't display any custom error messages on failed login // but that's actually desired to expose as little information as possible return $args; } /** * Helper method to verify the given method/code tuple */ protected function verify_factor_auth($method, $code) { if (strlen($code) && ($driver = $this->get_driver($method))) { // set properties from login $driver->username = $_SESSION['username']; try { // verify the submitted code return $driver->verify($code, $_SESSION['kolab_2fa_time']); } catch (Exception $e) { rcube::raise_error($e, true, false); } } return false; } /** * Render 2nd factor authentication form in place of the regular login form */ public function auth_form($attrib = array()) { $form_name = !empty($attrib['form']) ? $attrib['form'] : 'form'; $nonce = $_SESSION['kolab_2fa_nonce']; $methods = array_unique(array_map(function($factor) { list($method, $id) = explode(':', $factor); return $method; }, $this->login_factors )); // forward these values as the regular login screen would submit them $input_task = new html_hiddenfield(array('name' => '_task', 'value' => 'login')); $input_action = new html_hiddenfield(array('name' => '_action', 'value' => 'plugin.kolab-2fa-login')); $input_tzone = new html_hiddenfield(array('name' => '_timezone', 'id' => 'rcmlogintz', 'value' => rcube_utils::get_input_value('_timezone', rcube_utils::INPUT_POST))); $input_url = new html_hiddenfield(array('name' => '_url', 'id' => 'rcmloginurl', 'value' => rcube_utils::get_input_value('_url', rcube_utils::INPUT_POST))); // create HTML table with two cols $table = new html_table(array('cols' => 2)); $required = count($methods) > 1 ? null : 'required'; // render input for each configured auth method foreach ($methods as $i => $method) { if ($row++ > 0) { $table->add(array('colspan' => 2, 'class' => 'title hint', 'style' => 'text-align:center'), $this->gettext('or')); } $field_id = "rcmlogin2fa$method"; $input_code = new html_inputfield(array('name' => "_${nonce}_${method}", 'class' => 'kolab2facode', 'id' => $field_id, 'required' => $required, 'autocomplete' => 'off') + $attrib); $table->add('title', html::label($field_id, html::quote($this->gettext($method)))); $table->add('input', $input_code->show('')); } $out = $input_task->show(); $out .= $input_action->show(); $out .= $input_tzone->show(); $out .= $input_url->show(); $out .= $table->show(); // add submit button if (rcube_utils::get_boolean($attrib['submit'])) { $submit = new html_inputfield(array('type' => 'submit', 'id' => 'rcmloginsubmit', 'class' => 'button mainaction', 'value' => $this->gettext('continue'))); $out .= html::p('formbuttons', $submit->show()); } // surround html output with a form tag if (empty($attrib['form'])) { $out = $this->api->output->form_tag(array('name' => $form_name, 'method' => 'post'), $out); } return $out; } /** * Load driver class for the given method */ public function get_driver($method) { $rcmail = rcmail::get_instance(); if ($this->drivers[$method]) { return $this->drivers[$method]; } $config = $rcmail->config->get('kolab_2fa_' . $method, array()); // use product name as "issuer"" if (empty($config['issuer'])) { $config['issuer'] = $rcmail->config->get('product_name'); } try { // TODO: use external auth service if configured $driver = \Kolab2FA\Driver\Base::factory($method, $config); // attach storage $driver->storage = $this->get_storage(); - // set user properties from active session if ($rcmail->user->ID) { $driver->username = $rcmail->get_user_name(); } $this->drivers[$method] = $driver; return $driver; } catch (Exception $e) { $error = strval($e); } rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => $error), true, false); return false; } /** * Getter for a storage instance singleton */ public function get_storage($for = null) { if (!isset($this->storage) || (!empty($for) && $this->storage->username !== $for)) { $rcmail = rcmail::get_instance(); - try { $this->storage = \Kolab2FA\Storage\Base::factory( $rcmail->config->get('kolab_2fa_storage', 'roundcube'), $rcmail->config->get('kolab_2fa_storage_config', array()) ); + $this->storage->set_username($for); + + // set user properties from active session + if (!empty($_SESSION['kolab_dn'])) { + $this->storage->userdn = $_SESSION['kolab_dn']; + } } catch (Exception $e) { $this->storage = false; rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => $error), true, false); } } return $this->storage; } /** * Handler for 'settings_actions' hook */ public function settings_actions($args) { // register as settings action $args['actions'][] = array( 'action' => 'plugin.kolab-2fa', 'class' => '2factorauth', 'label' => 'settingslist', 'title' => 'settingstitle', 'domain' => 'kolab_2fa', ); return $args; } /** * Handler for settings/plugin.kolab-2fa requests */ public function settings_view() { $this->register_handler('plugin.settingsform', array($this, 'settings_form')); $this->register_handler('plugin.settingslist', array($this, 'settings_list')); $this->register_handler('plugin.factoradder', array($this, 'settings_factoradder')); $this->register_handler('plugin.highsecuritydialog', array($this, 'settings_highsecuritydialog')); $this->include_script('kolab2fa.js'); $this->include_stylesheet($this->local_skin_path() . '/kolab2fa.css'); if ($this->check_secure_mode()) { $this->api->output->set_env('session_secured', $_SESSION['kolab_2fa_secure_mode']); } $this->api->output->add_label('save','cancel'); $this->api->output->set_pagetitle($this->gettext('settingstitle')); $this->api->output->send('kolab_2fa.config'); } /** * Render the menu to add another authentication factor */ public function settings_factoradder($attrib) { $rcmail = rcmail::get_instance(); $select = new html_select(array('id' => 'kolab2fa-add')); $select->add($this->gettext('addfactor') . '...', ''); foreach ((array)$rcmail->config->get('kolab_2fa_drivers', array()) as $method) { $select->add($this->gettext($method), $method); } return $select->show(); } /** * Render a list of active factor this user has configured */ public function settings_list($attrib = array()) { $attrib['id'] = 'kolab2fa-factors'; $table = new html_table(array('cols' => 3)); $table->add_header('name', $this->gettext('factor')); $table->add_header('created', $this->gettext('created')); $table->add_header('actions', ''); return $table->show($attrib); } /** * Render the settings form template object */ public function settings_form($attrib = array()) { $rcmail = rcmail::get_instance(); $storage = $this->get_storage($rcmail->get_user_name()); $factors = $storage ? (array)$storage->enumerate() : array(); $drivers = (array)$rcmail->config->get('kolab_2fa_drivers', array()); $env_methods = array(); foreach ($drivers as $j => $method) { $out .= $this->settings_factor($method, $attrib); $env_methods[$method] = array( 'name' => $this->gettext($method), 'active' => 0, ); } $me = $this; $this->api->output->set_env('kolab_2fa_factors', array_combine( $factors, array_map(function($id) use ($me, &$env_methods) { $props = array('id' => $id); if ($driver = $me->get_driver($id)) { $props += $this->format_props($driver->props()); $props['method'] = $driver->method; $props['name'] = $me->gettext($driver->method); $env_methods[$driver->method]['active']++; } return $props; }, $factors) )); $this->api->output->set_env('kolab_2fa_methods', $env_methods); return html::div(array('id' => 'kolab2fapropform'), $out); } /** * Render the settings UI for the given method/driver */ protected function settings_factor($method, $attrib) { $out = ''; $rcmail = rcmail::get_instance(); $attrib += array('class' => 'propform'); if ($driver = $this->get_driver($method)) { $table = new html_table(array('cols' => 2, 'class' => $attrib['class'])); foreach ($driver->props() as $field => $prop) { if (!$prop['editable']) { continue; } switch ($prop['type']) { case 'boolean': case 'checkbox': $input = new html_checkbox(array('value' => '1')); break; case 'enum': case 'select': $input = new html_select(array('disabled' => $prop['readonly'])); $input->add(array_map(array($this, 'gettext'), $prop['options']), $prop['options']); break; default: $input = new html_inputfield(array('size' => $prop['size'] ?: 30, 'disabled' => !$prop['editable'])); } $explain_label = $field . 'explain' . $method; $explain_html = $rcmail->text_exists($explain_label, 'kolab_2fa') ? html::p('explain', $this->gettext($explain_label)) : ''; $field_id = 'rcmk2fa' . $method . $field; $table->add('title', html::label($field_id, $this->gettext($field))); $table->add(null, $input->show('', array('id' => $field_id, 'name' => "_prop[$field]")) . $explain_html); } // add row for displaying the QR code if (method_exists($driver, 'get_provisioning_uri')) { $table->add('title', $this->gettext('qrcode')); $table->add(null, html::p('explain', $this->gettext("qrcodeexplain$method") ) . html::p(null, html::tag('img', array('src' => 'data:image/gif;base64,R0lGODlhDwAPAIAAAMDAwAAAACH5BAEAAAAALAAAAAAPAA8AQAINhI+py+0Po5y02otnAQA7', 'class' => 'qrcode', 'rel' => $method)) ) ); // add row for testing the factor $field_id = 'rcmk2faverify' . $method; $table->add('title', html::label($field_id, $this->gettext('verifycode'))); $table->add(null, html::tag('input', array('type' => 'text', 'name' => '_verify_code', 'id' => $field_id, 'class' => 'k2fa-verify', 'size' => 20, 'required' => true)) . html::p('explain', $this->gettext("verifycodeexplain$method")) ); } $input_id = new html_hiddenfield(array('name' => '_prop[id]', 'value' => '')); $out .= html::tag('form', array( 'method' => 'post', 'action' => '#', 'id' => 'kolab2fa-prop-' . $method, 'style' => 'display:none', ), html::tag('fieldset', array(), html::tag('legend', array(), $this->gettext($method)) . html::div('factorprop', $table->show()) . $input_id->show() ) ); } return $out; } /** * Render th */ public function settings_highsecuritydialog($attrib = array()) { $attrib += array('id' => 'kolab2fa-highsecuritydialog'); $field_id = 'rcmk2facode'; $input = new html_inputfield(array('name' => '_code', 'id' => $field_id, 'class' => 'verifycode', 'size' => 20)); return html::div($attrib, html::p('explain', $this->gettext('highsecuritydialog')) . html::div('propform', html::label($field_id, '$name') . $input->show('')) ); } /** * Handler for settings/plugin.kolab-2fa-save requests */ public function settings_save() { $method = rcube_utils::get_input_value('_method', rcube_utils::INPUT_POST); $data = @json_decode(rcube_utils::get_input_value('_data', rcube_utils::INPUT_POST), true); $rcmail = rcmail::get_instance(); $storage = $this->get_storage($rcmail->get_user_name()); $success = false; $errors = 0; $save_data = array(); if ($driver = $this->get_driver($method)) { if ($data === false) { if ($this->check_secure_mode()) { // remove method from active factors and clear stored settings $success = $driver->clear(); } else { $errors++; } } else { // verify the submitted code before saving $verify_code = rcube_utils::get_input_value('_verify_code', rcube_utils::INPUT_POST); $timestamp = intval(rcube_utils::get_input_value('_timestamp', rcube_utils::INPUT_POST)); if (!empty($verify_code)) { if (!$driver->verify($verify_code, $timestamp)) { $this->api->output->command('plugin.verify_response', array( 'id' => $driver->id, 'method' => $driver->method, 'success' => false, 'message' => str_replace('$method', $this->gettext($driver->method), $this->gettext('codeverificationfailed')) )); $this->api->output->send(); } } foreach ($data as $prop => $value) { if (!$driver->set($prop, $value)) { $errors++; } } $driver->set('active', true); } // update list of active factors for this user if (!$errors) { $success = $driver->commit(); $save_data = $data !== false ? $this->format_props($driver->props()) : array(); } } if ($success) { $this->api->output->show_message($data === false ? $this->gettext('factorremovesuccess') : $this->gettext('factorsavesuccess'), 'confirmation'); $this->api->output->command('plugin.save_success', array('method' => $method, 'active' => $data !== false) + $save_data); } else if ($errors) { $this->api->output->show_message($this->gettext('factorsaveerror'), 'error'); $this->api->output->command('plugin.reset_form', $method); } $this->api->output->send(); } /** * Handler for settings/plugin.kolab-2fa-data requests */ public function settings_data() { $method = rcube_utils::get_input_value('_method', rcube_utils::INPUT_POST); if ($driver = $this->get_driver($method)) { $data = array('method' => $method, 'id' => $driver->id); foreach ($driver->props(true) as $field => $prop) { $data[$field] = $prop['text'] ?: $prop['value']; } // generate QR code for provisioning URI if (method_exists($driver, 'get_provisioning_uri')) { try { $uri = $driver->get_provisioning_uri(); $qr = new Endroid\QrCode\QrCode(); $qr->setText($uri) ->setSize(240) ->setPadding(10) ->setErrorCorrection('high') ->setForegroundColor(array('r' => 0, 'g' => 0, 'b' => 0, 'a' => 0)) ->setBackgroundColor(array('r' => 255, 'g' => 255, 'b' => 255, 'a' => 0)); $data['qrcode'] = base64_encode($qr->get()); } catch (Exception $e) { rcube::raise_error($e, true, false); } } $this->api->output->command('plugin.render_data', $data); } $this->api->output->send(); } /** * Handler for settings/plugin.kolab-2fa-verify requests */ public function settings_verify() { $method = rcube_utils::get_input_value('_method', rcube_utils::INPUT_POST); $timestamp = intval(rcube_utils::get_input_value('_timestamp', rcube_utils::INPUT_POST)); $success = false; if ($driver = $this->get_driver($method)) { $data = @json_decode(rcube_utils::get_input_value('_data', rcube_utils::INPUT_POST), true); if (is_array($data)) { foreach ($data as $key => $value) { if ($value !== '******') { $driver->$key = $value; } } } $success = $driver->verify(rcube_utils::get_input_value('_code', rcube_utils::INPUT_POST), $timestamp); $method = $driver->method; } // put session into high-security mode if ($success && !empty($_POST['_session'])) { $_SESSION['kolab_2fa_secure_mode'] = time(); } $this->api->output->command('plugin.verify_response', array( 'method' => $method, 'success' => $success, 'message' => str_replace('$method', $this->gettext($method), $this->gettext($success ? 'codeverificationpassed' : 'codeverificationfailed')) )); $this->api->output->send(); } /** * */ protected function format_props($props) { $rcmail = rcmail::get_instance(); $values = array(); foreach ($props as $key => $prop) { switch ($prop['type']) { case 'datetime': $value = $rcmail->format_date($prop['value']); break; default: $value = $prop['value']; } $values[$key] = $value; } return $values; } /** * */ protected function check_secure_mode() { $valid = ($_SESSION['kolab_2fa_secure_mode'] && $_SESSION['kolab_2fa_secure_mode'] > time() - 180); return $valid; } } \ No newline at end of file diff --git a/plugins/kolab_2fa/lib/Kolab2FA/Driver/Base.php b/plugins/kolab_2fa/lib/Kolab2FA/Driver/Base.php index 89488231..b6a7080a 100644 --- a/plugins/kolab_2fa/lib/Kolab2FA/Driver/Base.php +++ b/plugins/kolab_2fa/lib/Kolab2FA/Driver/Base.php @@ -1,352 +1,346 @@ * * Copyright (C) 2015, Kolab Systems AG * * This program 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. * * This program 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 this program. If not, see . */ namespace Kolab2FA\Driver; abstract class Base { public $method = null; public $id = null; public $storage; protected $config = array(); protected $props = array(); protected $user_props = array(); protected $pending_changes = false; + protected $temporary = false; protected $allowed_props = array('username'); public $user_settings = array( 'active' => array( 'type' => 'boolean', 'editable' => false, 'hidden' => false, 'default' => false, ), 'label' => array( 'type' => 'text', 'editable' => true, 'label' => 'label', 'generator' => 'default_label', ), 'created' => array( 'type' => 'datetime', 'editable' => false, 'hidden' => false, 'label' => 'created', 'generator' => 'time', ), ); /** * Static factory method */ public static function factory($id, $config) { list($method) = explode(':', $id); $classmap = array( 'totp' => '\\Kolab2FA\\Driver\\TOTP', 'hotp' => '\\Kolab2FA\\Driver\\HOTP', 'yubikey' => '\\Kolab2FA\\Driver\\Yubikey', ); $cls = $classmap[strtolower($method)]; if ($cls && class_exists($cls)) { return new $cls($config, $id); } throw new Exception("Unknown 2FA driver '$method'"); } /** * Default constructor */ public function __construct($config = null, $id = null) { $this->init($config); if (!empty($id) && $id != $this->method) { $this->id = $id; } else { // generate random ID $this->id = $this->method . ':' . bin2hex(openssl_random_pseudo_bytes(12)); + $this->temporary = true; } } /** * Initialize the driver with the given config options */ public function init($config) { if (is_array($config)) { $this->config = array_merge($this->config, $config); } if ($config['storage']) { $this->storage = \Kolab2FA\Storage\Base::factory($config['storage'], $config['storage_config']); } } /** * Verify the submitted authentication code * * @param string $code The 2nd authentication factor to verify * @param int $timestamp Timestamp of authentication process (window start) * @return boolean True if valid, false otherwise */ abstract function verify($code, $timestamp = null); /** * Getter for user-visible properties */ public function props($force = false) { $data = array(); foreach ($this->user_settings as $key => $p) { if ($p['private']) { continue; } $data[$key] = array( 'type' => $p['type'], 'editable' => $p['editable'], 'hidden' => $p['hidden'], 'label' => $p['label'], 'value' => $this->get($key, $force), ); // format value into text switch ($p['type']) { case 'boolean': $data[$key]['value'] = (bool)$data[$key]['value']; $data[$key]['text'] = $data[$key]['value'] ? 'yes' : 'no'; break; case 'datetime': if (is_numeric($data[$key]['value'])) { $data[$key]['text'] = date('c', $data[$key]['value']); break; } default: $data[$key]['text'] = $data[$key]['value']; } } return $data; } /** * Implement this method if the driver can be prpvisioned via QR code */ /* abstract function get_provisioning_uri(); */ /** * Generate a random secret string */ public function generate_secret($length = 16) { // Base32 characters $chars = array( 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23 'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31 ); $secret = ''; for ($i = 0; $i < $length; $i++) { $secret .= $chars[array_rand($chars)]; } return $secret; } /** * Generate the default label based on the method */ public function default_label() { if (class_exists('\\rcmail', false)) { return \rcmail::get_instance()->gettext($this->method, 'kolab_2fa'); } return strtoupper($this->method); } /** * Getter for read-only access to driver properties */ public function get($key, $force = false) { // this is a per-user property: get from persistent storage if (isset($this->user_settings[$key])) { $value = $this->get_user_prop($key); // generate property value if (!isset($value) && $force && $this->user_settings[$key]['generator']) { $func = $this->user_settings[$key]['generator']; if (is_string($func) && !is_callable($func)) { $func = array($this, $func); } if (is_callable($func)) { $value = call_user_func($func); } if (isset($value)) { $this->set_user_prop($key, $value); } } } else { $value = $this->props[$key]; } return $value; } /** * Setter for restricted access to driver properties */ public function set($key, $value, $persistent = true) { // store as per-user property if (isset($this->user_settings[$key])) { if ($persistent) { return $this->set_user_prop($key, $value); } $this->user_props[$key] = $value; } $setter = 'set_' . $key; if (method_exists($this, $setter)) { call_user_func(array($this, $setter), $value); } else if (in_array($key, $this->allowed_props)) { $this->props[$key] = $value; } return true; } /** * Commit changes to storage */ public function commit() { if (!empty($this->user_props) && $this->storage && $this->pending_changes) { if ($this->storage->write($this->id, $this->user_props)) { $this->pending_changes = false; + $this->temporary = false; } } return !$this->pending_changes; } /** * Dedicated setter for the username property */ public function set_username($username) { $this->props['username'] = $username; if ($this->storage) { $this->storage->set_username($username); } return true; } /** * Clear data stored for this driver */ public function clear() { if ($this->storage) { return $this->storage->remove($this->id); } return false; } /** * Getter for per-user properties for this method */ protected function get_user_prop($key) { - if (!isset($this->user_props[$key]) && $this->storage && !$this->pending_changes) { + if (!isset($this->user_props[$key]) && $this->storage && !$this->pending_changes && !$this->temporary) { $this->user_props = (array)$this->storage->read($this->id); } return $this->user_props[$key]; } /** * Setter for per-user properties for this method */ protected function set_user_prop($key, $value) { - $success = true; $this->pending_changes |= ($this->user_props[$key] !== $value); $this->user_props[$key] = $value; - -/* - if ($this->user_settings[$key] && $this->storage) { - $props = (array)$this->storage->read($this->id); - $props[$key] = $value; - $success = $this->storage->write($this->id, $props); - } -*/ - return $success; + return true; } /** * Magic getter for read-only access to driver properties */ public function __get($key) { // this is a per-user property: get from persistent storage if (isset($this->user_settings[$key])) { return $this->get_user_prop($key); } return $this->props[$key]; } /** * Magic setter for restricted access to driver properties */ public function __set($key, $value) { $this->set($key, $value, false); } /** * Magic check if driver property is defined */ public function __isset($key) { return isset($this->props[$key]); } } \ No newline at end of file diff --git a/plugins/kolab_2fa/lib/Kolab2FA/Driver/HOTP.php b/plugins/kolab_2fa/lib/Kolab2FA/Driver/HOTP.php index 89542242..b51e0052 100644 --- a/plugins/kolab_2fa/lib/Kolab2FA/Driver/HOTP.php +++ b/plugins/kolab_2fa/lib/Kolab2FA/Driver/HOTP.php @@ -1,121 +1,129 @@ * * Copyright (C) 2015, Kolab Systems AG * * This program 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. * * This program 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 this program. If not, see . */ namespace Kolab2FA\Driver; class HOTP extends Base { public $method = 'hotp'; protected $config = array( 'digits' => 6, 'window' => 4, 'digest' => 'sha1', ); protected $backend; /** * */ public function init($config) { parent::init($config); $this->user_settings += array( 'secret' => array( 'type' => 'text', 'private' => true, 'label' => 'secret', 'generator' => 'generate_secret', ), 'counter' => array( 'type' => 'integer', 'editable' => false, 'hidden' => true, 'generator' => 'random_counter', ), ); // copy config options $this->backend = new \Kolab2FA\OTP\HOTP(); $this->backend ->setDigits($this->config['digits']) ->setDigest($this->config['digest']) ->setIssuer($this->config['issuer']) ->setIssuerIncludedAsParameter(true); } /** * */ public function verify($code, $timestamp = null) { // get my secret from the user storage $secret = $this->get('secret'); $counter = $this->get('counter'); if (!strlen($secret)) { // LOG: "no secret set for user $this->username" console("VERIFY HOTP: no secret set for user $this->username"); return false; } - $this->backend->setLabel($this->username)->setSecret($secret)->setCounter($this->get('counter')); - $pass = $this->backend->verify($code, $counter, $this->config['window']); + try { + $this->backend->setLabel($this->username)->setSecret($secret)->setCounter(intval($this->get('counter'))); + $pass = $this->backend->verify($code, $counter, $this->config['window']); - // store incremented counter value - $this->set('counter', $this->backend->getCounter()); + // store incremented counter value + $this->set('counter', $this->backend->getCounter()); + $this->commit(); + } + catch (\Exception $e) { + // LOG: exception + console("VERIFY HOTP: $this->id, " . strval($e)); + $pass = false; + } // console('VERIFY HOTP', $this->username, $secret, $counter, $code, $pass); return $pass; } /** * */ public function get_provisioning_uri() { if (!$this->secret) { // generate new secret and store it $this->set('secret', $this->get('secret', true)); $this->set('counter', $this->get('counter', true)); $this->set('created', $this->get('created', true)); $this->commit(); } // TODO: deny call if already active? - $this->backend->setLabel($this->username)->setSecret($this->secret)->setCounter($this->get('counter')); + $this->backend->setLabel($this->username)->setSecret($this->secret)->setCounter(intval($this->get('counter'))); return $this->backend->getProvisioningUri(); } /** * Generate a random counter value */ public function random_counter() { return mt_rand(1, 999); } } diff --git a/plugins/kolab_2fa/lib/Kolab2FA/Storage/Base.php b/plugins/kolab_2fa/lib/Kolab2FA/Storage/Base.php index ba9ac8a7..f79f2d57 100644 --- a/plugins/kolab_2fa/lib/Kolab2FA/Storage/Base.php +++ b/plugins/kolab_2fa/lib/Kolab2FA/Storage/Base.php @@ -1,95 +1,95 @@ * * Copyright (C) 2015, Kolab Systems AG * * This program 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. * * This program 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 this program. If not, see . */ namespace Kolab2FA\Storage; abstract class Base { public $username = null; protected $config = array(); /** * */ public static function factory($backend, $config) { $classmap = array( 'ldap' => '\\Kolab2FA\\Storage\\LDAP', 'roundcube' => '\\Kolab2FA\\Storage\\RcubeUser', 'rcubeuser' => '\\Kolab2FA\\Storage\\RcubeUser', ); $cls = $classmap[strtolower($backend)]; if ($cls && class_exists($cls)) { return new $cls($config); } throw new Exception("Unknown storage backend '$backend'"); } /** * Default constructor */ public function __construct($config = null) { if (is_array($config)) { $this->init($config); } } /** * Initialize the driver with the given config options */ public function init(array $config) { $this->config = array_merge($this->config, $config); } /** * Set username to store data for */ public function set_username($username) { $this->username = $username; } /** * List keys holding settings for 2-factor-authentication */ abstract public function enumerate(); /** * Read data for the given key */ abstract public function read($key); /** * Save data for the given key */ abstract public function write($key, $value); /** - * Remove the data stoed for the given key + * Remove the data stored for the given key */ abstract public function remove($key); } diff --git a/plugins/kolab_2fa/lib/Kolab2FA/Storage/LDAP.php b/plugins/kolab_2fa/lib/Kolab2FA/Storage/LDAP.php index c8546bb9..67871adf 100644 --- a/plugins/kolab_2fa/lib/Kolab2FA/Storage/LDAP.php +++ b/plugins/kolab_2fa/lib/Kolab2FA/Storage/LDAP.php @@ -1,261 +1,374 @@ * * Copyright (C) 2015, Kolab Systems AG * * This program 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. * * This program 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 this program. If not, see . */ namespace Kolab2FA\Storage; use \Net_LDAP3; class LDAP extends Base { + public $userdn; + private $cache = array(); - private $users = array(); + private $ldapcache = array(); private $conn; private $error; public function init(array $config) { parent::init($config); $this->conn = new Net_LDAP3($config); $this->conn->config_set('log_hook', array($this, 'log')); $this->conn->connect(); $bind_pass = $this->config['bind_pass']; $bind_user = $this->config['bind_user']; $bind_dn = $this->config['bind_dn']; $this->ready = $this->conn->bind($bind_dn, $bind_pass); if (!$this->ready) { throw new Exception("LDAP storage not ready: " . $this->error); } } + /** + * List/set methods activated for this user + */ + public function enumerate($active = true) + { + $filter = $this->parse_vars($this->config['filter'], '*'); + $base_dn = $this->parse_vars($this->config['base_dn'], '*'); + $scope = $this->config['scope'] ?: 'sub'; + $ids = array(); + + if ($this->ready && ($result = $this->conn->search($base_dn, $filter, $scope, array($this->config['fieldmap']['id'], $this->config['fieldmap']['active'])))) { + foreach ($result as $dn => $entry) { + $rec = $this->field_mapping($dn, Net_LDAP3::normalize_entry($entry, true)); + if (!empty($rec['id']) && ($active === null || $active == $rec['active'])) { + $ids[] = $rec['id']; + } + } + } + + // TODO: cache this in memory + + return $ids; + } + /** * Read data for the given key */ public function read($key) { - if (!isset($this->cache[$key]) && ($rec = $this->get_ldap_record($this->username, $key))) { - $pkey = '@' . $key; - if (!empty($this->config['fieldmap'][$pkey])) { - $rec = @json_decode($rec[$pkey], true); - } - else if ($this->config['fieldmap'][$key]) { - $rec = $rec[$key]; - } - $this->cache[$key] = $rec; + if (!isset($this->cache[$key])) { + $this->cache[$key] = $this->get_ldap_record($this->username, $key); } return $this->cache[$key]; } /** * Save data for the given key */ public function write($key, $value) { - if ($rec = $this->get_ldap_record($this->username, $key)) { - $old_attrs = $rec['_raw']; - $new_attrs = $old_attrs; + $success = false; + $ldap_attrs = array(); - // serialize $value into one attribute - $pkey = '@' . $key; - if ($attr = $this->config['fieldmap'][$pkey]) { - $new_attrs[$attr] = $value === null ? '' : json_encode($value); - } - else if ($attr = $this->config['fieldmap'][$key]) { - $new_attrs[$attr] = $this->value_mapping($attr, $value, false); - - // special case nsroledn: keep other roles unknown to us - if ($attr == 'nsroledn' && is_array($this->config['valuemap'][$attr])) { - $map = $this->config['valuemap'][$attr]; - $new_attrs[$attr] = array_merge( - $new_attrs[$attr], - array_filter((array)$old_attrs[$attr], function($f) use ($map) { return !in_array($f, $map); }) - ); - } - } - else if (is_array($value)) { - foreach ($value as $k => $val) { - if ($attr = $this->config['fieldmap'][$k]) { - $new_attrs[$attr] = $this->value_mapping($attr, $value, false); - } + if (is_array($value)) { + // add some default values + $value += (array)$this->config['defaults'] + array('active' => false, 'username' => $this->username, 'userdn' => $this->userdn); + + foreach ($value as $k => $val) { + if ($attr = $this->config['fieldmap'][$k]) { + $ldap_attrs[$attr] = $this->value_mapping($k, $val, false); } } + } + else { + // invalid data structure + return false; + } + + // update existing record + if ($rec = $this->get_ldap_record($this->username, $key)) { + $old_attrs = $rec['_raw']; + $new_attrs = array_merge($old_attrs, $ldap_attrs); $result = $this->conn->modify_entry($rec['_dn'], $old_attrs, $new_attrs); + $success = !empty($result); + } + // insert new record + else if ($this->ready) { + $entry_dn = $this->get_entry_dn($this->username, $key); - if (!empty($result)) { - $this->cache[$key] = $value; - $this->users = array(); - } + // add object class attribute + $me = $this; + $ldap_attrs['objectclass'] = array_map(function($cls) use ($me, $key) { + return $me->parse_vars($cls, $key); + }, (array)$this->config['objectclass']); - return !empty($result); + $success = $this->conn->add_entry($entry_dn, $ldap_attrs); } - return false; + if ($success) { + $this->cache[$key] = $value; + $this->ldapcache = array(); + + // cleanup: remove disabled/inactive/temporary entries + if ($value['active']) { + foreach ($this->enumerate(false) as $id) { + if ($id != $key) { + $this->remove($id); + } + } + + // set user roles according to active factors + $this->set_user_roles(); + } + } + + return $success; } /** - * Remove the data stoed for the given key + * Remove the data stored for the given key */ public function remove($key) { - return $this->write($key, null); + if ($this->ready) { + $entry_dn = $this->get_entry_dn($this->username, $key); + $success = $this->conn->delete_entry($entry_dn); + + // set user roles according to active factors + if ($success) { + $this->set_user_roles(); + } + + return $success; + } + + return false; } /** * Set username to store data for */ public function set_username($username) { parent::set_username($username); // reset cached values $this->cache = array(); - $this->users = array(); + $this->ldapcache = array(); + } + + /** + * + */ + protected function set_user_roles() + { + if (!$this->ready || !$this->userdn || empty($this->config['user_roles'])) { + return false; + } + + $auth_roles = array(); + foreach ($this->enumerate(true) as $id) { + foreach ($this->config['user_roles'] as $prefix => $role) { + if (strpos($id, $prefix) === 0) { + $auth_roles[] = $role; + } + } + } + + $role_attr = $this->config['fieldmap']['roles'] ?: 'nsroledn'; + if ($user_attrs = $this->conn->get_entry($this->userdn, array($role_attr))) { + $internals = array_values($this->config['user_roles']); + $new_attrs = $old_attrs = Net_LDAP3::normalize_entry($user_attrs); + $new_attrs[$role_attr] = array_merge( + array_unique($auth_roles), + array_filter((array)$old_attrs[$role_attr], function($f) use ($internals) { return !in_array($f, $internals); }) + ); + + $result = $this->conn->modify_entry($this->userdn, $old_attrs, $new_attrs); + return !empty($result); + } + + return false; } /** * Fetches user data from LDAP addressbook */ protected function get_ldap_record($user, $key) { - $filter = $this->parse_vars($this->config['filter'], $user, $key); - $base_dn = $this->parse_vars($this->config['base_dn'], $user, $key); - $scope = $this->config['scope'] ?: 'sub'; + $entry_dn = $this->get_entry_dn($user, $key); - $cachekey = $base_dn . $filter; - if (!isset($this->users[$cachekey])) { - $this->users[$cachekey] = array(); + if (!isset($this->ldapcache[$entry_dn])) { + $this->ldapcache[$entry_dn] = array(); - if ($this->ready && ($result = $this->conn->search($base_dn, $filter, $scope, array_values($this->config['fieldmap'])))) { - if ($result->count() == 1) { - $entries = $result->entries(true); - $dn = key($entries); - $entry = array_pop($entries); - $this->users[$cachekey] = $this->field_mapping($dn, $entry); - } + if ($this->ready && ($entry = $this->conn->get_entry($entry_dn, array_values($this->config['fieldmap'])))) { + $this->ldapcache[$entry_dn] = $this->field_mapping($entry_dn, Net_LDAP3::normalize_entry($entry, true)); } } - return $this->users[$cachekey]; + return $this->ldapcache[$entry_dn]; + } + + /** + * Compose a full DN for the given record identifier + */ + protected function get_entry_dn($user, $key) + { + $base_dn = $this->parse_vars($this->config['base_dn'], $key); + return sprintf('%s=%s,%s', $this->config['rdn'], Net_LDAP3::quote_string($key, true), $base_dn); } /** * Maps LDAP attributes to defined fields */ protected function field_mapping($dn, $entry) { $entry['_dn'] = $dn; $entry['_raw'] = $entry; // fields mapping foreach ($this->config['fieldmap'] as $field => $attr) { $attr_lc = strtolower($attr); if (isset($entry[$attr_lc])) { - $entry[$field] = $this->value_mapping($attr_lc, $entry[$attr_lc], true); + $entry[$field] = $this->value_mapping($field, $entry[$attr_lc], true); } else if (isset($entry[$attr])) { - $entry[$field] = $this->value_mapping($attr, $entry[$attr], true); + $entry[$field] = $this->value_mapping($field, $entry[$attr], true); } } return $entry; } /** * */ protected function value_mapping($attr, $value, $reverse = false) { if ($map = $this->config['valuemap'][$attr]) { if ($reverse) { $map = array_flip($map); } if (is_array($value)) { $value = array_filter(array_map(function($val) use ($map) { return $map[$val]; }, $value)); } else { $value = $map[$value]; } } + // convert (date) type + switch ($this->config['attrtypes'][$attr]) { + case 'datetime': + $ts = is_numeric($value) ? $value : strtotime($value); + if ($ts) { + $value = gmdate($reverse ? 'U' : 'YmdHi\Z', $ts); + } + break; + + case 'integer': + $value = intval($value); + break; + } + return $value; } /** * Prepares filter query for LDAP search */ - protected function parse_vars($str, $user, $key) + protected function parse_vars($str, $key) { - // replace variables in filter - list($u, $d) = explode('@', $user); + $user = $this->username; + + if (strpos($user, '@') > 0) { + list($u, $d) = explode('@', $user); + } + else if ($this->userdn) { + $u = $this->userdn; + $d = trim(str_replace(',dc=', '.', substr($u, strpos($u, ',dc='))), '.'); + } + + if ($this->userdn) { + $user = $this->userdn; + } // build hierarchal domain string $dc = $this->conn->domain_root_dn($d); - // map key value - if (is_array($this->config['keymap']) && isset($this->config['keymap'][$key])) { - $key = $this->config['keymap'][$key]; - } + $class = $this->config['classmap'] ? $this->config['classmap']['*'] : '*'; - // TODO: resolve $user into its DN for %udn + // map key to objectclass + if (is_array($this->config['classmap'])) { + foreach ($this->config['classmap'] as $k => $c) { + if (strpos($key, $k) === 0) { + $class = $c; + break; + } + } + } - $replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u, '%k' => $key); + $replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u, '%c' => $class); return strtr($str, $replaces); } /** * Prints debug/error info to the log */ public function log($level, $msg) { $msg = implode("\n", $msg); switch ($level) { case LOG_DEBUG: case LOG_INFO: case LOG_NOTICE: if ($this->config['debug'] && class_exists('\\rcube', false)) { \rcube::write_log('ldap', $msg); } break; case LOG_EMERGE: case LOG_ALERT: case LOG_CRIT: case LOG_ERR: case LOG_WARNING: $this->error = $msg; // throw new Exception("LDAP storage error: " . $msg); break; } } } diff --git a/plugins/kolab_2fa/lib/Kolab2FA/Storage/RcubeUser.php b/plugins/kolab_2fa/lib/Kolab2FA/Storage/RcubeUser.php index f0664bb1..5f614704 100644 --- a/plugins/kolab_2fa/lib/Kolab2FA/Storage/RcubeUser.php +++ b/plugins/kolab_2fa/lib/Kolab2FA/Storage/RcubeUser.php @@ -1,183 +1,183 @@ * * Copyright (C) 2015, Kolab Systems AG * * This program 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. * * This program 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 this program. If not, see . */ namespace Kolab2FA\Storage; use \rcmail; use \rcube_user; class RcubeUser extends Base { // sefault config protected $config = array( 'keymap' => array(), ); private $cache = array(); private $user; public function init(array $config) { parent::init($config); $rcmail = rcmail::get_instance(); $this->config['hostname'] = $rcmail->user->ID ? $rcmail->user->data['mail_host'] : $_SESSION['hostname']; } /** * List/set methods activated for this user */ public function enumerate() { if ($factors = $this->get_factors()) { return array_keys(array_filter($factors, function($prop) { return !empty($prop['active']); })); } return array(); } /** * Read data for the given key */ public function read($key) { if (!isset($this->cache[$key])) { $factors = $this->get_factors(); console('READ', $key, $factors); $this->cache[$key] = $factors[$key]; } return $this->cache[$key]; } /** * Save data for the given key */ public function write($key, $value) { if ($user = $this->get_user($this->username)) { $this->cache[$key] = $value; $factors = $this->get_factors(); $factors[$key] = $value; $pkey = $this->key2property('blob'); $save_data = array($pkey => $factors); $update_index = false; // remove entry if ($value === null) { unset($factors[$key]); $update_index = true; } // remove non-active entries else if (!empty($value['active'])) { $factors = array_filter($factors, function($prop) { return !empty($prop['active']); }); $update_index = true; } // update the index of active factors if ($update_index) { $save_data[$this->key2property('factors')] = array_keys( array_filter($factors, function($prop) { return !empty($prop['active']); }) ); } return $user->save_prefs($save_data, true); } return false; } /** - * Remove the data stoed for the given key + * Remove the data stored for the given key */ public function remove($key) { return $this->write($key, null); } /** * Set username to store data for */ public function set_username($username) { parent::set_username($username); // reset cached values $this->cache = array(); $this->user = null; } /** * Helper method to get a rcube_user instance for storing prefs */ private function get_user($username) { // use global instance if we have a valid Roundcube session $rcmail = rcmail::get_instance(); if ($rcmail->user->ID && $rcmail->user->get_username() == $username) { return $rcmail->user; } if (!$this->user) { $this->user = rcube_user::query($username, $this->config['hostname']); } return $this->user; } /** * */ private function get_factors() { if ($user = $this->get_user($this->username)) { $prefs = $user->get_prefs(); return (array)$prefs[$this->key2property('blob')]; } return null; } /** * */ private function key2property($key) { // map key to configured property name if (is_array($this->config['keymap']) && isset($this->config['keymap'][$key])) { return $this->config['keymap'][$key]; } // default return 'kolab_2fa_' . $key; } }