From 7c828ae5f8ab20f0daa8a1482fadce9b3e858975 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 18 May 2010 10:39:56 -0700 Subject: [PATCH] OpenID access control options: trusted provider URL, Launchpad team restrictions. Added an admin panel for setting these and OpenID-only mode, off by default. To enable the admin panel: $config['admin']['panels'][] = 'openid'; Or to set them manually: $config['openid']['trusted_provider'] = 'https://login.ubuntu.net/'; $config['openid']['required_team'] = 'my-project-cabal'; $config['site']['openidonly'] = true; OpenID-only mode can still be set from addPlugin() parameters as well for backwards compatibility. Note: if it's set there, that value will override the setting from the database or config.php. Note that team restrictions are only really meaningful if a trusted provider is set; otherwise, any OpenID server could report back that users are members of the given team. Restrictions are checked only at OpenID authentication time and will not kick off people currently with a session open; existing remembered logins may also survive these changes. Using code for Launchpad team support provided by Canonical under AGPLv3, pulled from r27 of WordPress teams integration plugin: https://code.edge.launchpad.net/~canonical-isd-hackers/wordpress-teams-integration/trunk --- plugins/OpenID/OpenIDPlugin.php | 67 +++++- plugins/OpenID/extlib/README | 6 + plugins/OpenID/extlib/teams-extension.php | 175 ++++++++++++++ plugins/OpenID/finishaddopenid.php | 6 + plugins/OpenID/finishopenidlogin.php | 6 + plugins/OpenID/openid.php | 36 +++ plugins/OpenID/openidadminpanel.php | 270 ++++++++++++++++++++++ plugins/OpenID/openidlogin.php | 22 +- plugins/OpenID/openidsettings.php | 70 +++--- 9 files changed, 611 insertions(+), 47 deletions(-) create mode 100644 plugins/OpenID/extlib/README create mode 100644 plugins/OpenID/extlib/teams-extension.php create mode 100644 plugins/OpenID/openidadminpanel.php diff --git a/plugins/OpenID/OpenIDPlugin.php b/plugins/OpenID/OpenIDPlugin.php index 270e2c624b..9eac9f6fcd 100644 --- a/plugins/OpenID/OpenIDPlugin.php +++ b/plugins/OpenID/OpenIDPlugin.php @@ -20,7 +20,7 @@ * @category Plugin * @package StatusNet * @author Evan Prodromou - * @copyright 2009 StatusNet, Inc. + * @copyright 2009-2010 StatusNet, Inc. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ @@ -45,7 +45,19 @@ if (!defined('STATUSNET')) { class OpenIDPlugin extends Plugin { - public $openidOnly = false; + // Plugin parameter: set true to disallow non-OpenID logins + // If set, overrides the setting in database or $config['site']['openidonly'] + public $openidOnly = null; + + function initialize() + { + parent::initialize(); + if ($this->openidOnly !== null) { + global $config; + $config['site']['openidonly'] = (bool)$this->openidOnly; + } + + } /** * Add OpenID-related paths to the router table @@ -67,6 +79,7 @@ class OpenIDPlugin extends Plugin $m->connect('index.php?action=finishaddopenid', array('action' => 'finishaddopenid')); $m->connect('main/openidserver', array('action' => 'openidserver')); + $m->connect('admin/openid', array('action' => 'openidadminpanel')); return true; } @@ -84,7 +97,7 @@ class OpenIDPlugin extends Plugin function onStartConnectPath(&$path, &$defaults, &$rules, &$result) { - if ($this->openidOnly) { + if (common_config('site', 'openidonly')) { static $block = array('main/login', 'main/register', 'main/recoverpassword', @@ -108,7 +121,7 @@ class OpenIDPlugin extends Plugin function onArgsInitialize($args) { - if ($this->openidOnly) { + if (common_config('site', 'openidonly')) { if (array_key_exists('action', $args)) { $action = trim($args['action']); if (in_array($action, array('login', 'register'))) { @@ -199,7 +212,7 @@ class OpenIDPlugin extends Plugin function onStartPrimaryNav($action) { - if ($this->openidOnly && !common_logged_in()) { + if (common_config('site', 'openidonly') && !common_logged_in()) { // TRANS: Tooltip for main menu option "Login" $tooltip = _m('TOOLTIP', 'Login to the site'); // TRANS: Main menu option when not logged in to log in @@ -241,7 +254,7 @@ class OpenIDPlugin extends Plugin function onStartLoginGroupNav(&$action) { - if ($this->openidOnly) { + if (common_config('site', 'openidonly')) { $this->showOpenIDLoginTab($action); // Even though we replace this code, we // DON'T run the End* hook, to keep others from @@ -297,7 +310,7 @@ class OpenIDPlugin extends Plugin */ function onStartAccountSettingsPasswordMenuItem($menu, &$unused) { - if ($this->openidOnly) { + if (common_config('site', 'openidonly')) { return false; } return true; @@ -345,13 +358,19 @@ class OpenIDPlugin extends Plugin case 'OpenidsettingsAction': case 'OpenidserverAction': case 'OpenidtrustAction': - require_once INSTALLDIR.'/plugins/OpenID/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; + case 'OpenidadminpanelAction': + require_once dirname(__FILE__) . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; return false; case 'User_openid': - require_once INSTALLDIR.'/plugins/OpenID/User_openid.php'; + require_once dirname(__FILE__) . '/User_openid.php'; return false; case 'User_openid_trustroot': - require_once INSTALLDIR.'/plugins/OpenID/User_openid_trustroot.php'; + require_once dirname(__FILE__) . '/User_openid_trustroot.php'; + return false; + case 'Auth_OpenID_TeamsExtension': + case 'Auth_OpenID_TeamsRequest': + case 'Auth_OpenID_TeamsResponse': + require_once dirname(__FILE__) . '/extlib/teams-extension.php'; return false; default: return true; @@ -442,7 +461,7 @@ class OpenIDPlugin extends Plugin function onRedirectToLogin($action, $user) { - if ($this->openidOnly || (!empty($user) && User_openid::hasOpenID($user->id))) { + if (common_config('site', 'openid_only') || (!empty($user) && User_openid::hasOpenID($user->id))) { common_redirect(common_local_url('openidlogin'), 303); return false; } @@ -577,6 +596,32 @@ class OpenIDPlugin extends Plugin return true; } + /** + * Add an OpenID tab to the admin panel + * + * @param Widget $nav Admin panel nav + * + * @return boolean hook value + */ + + function onEndAdminPanelNav($nav) + { + if (AdminPanelAction::canAdmin('openid')) { + + $action_name = $nav->action->trimmed('action'); + + $nav->out->menuItem( + common_local_url('openidadminpanel'), + _m('OpenID'), + _m('OpenID configuration'), + $action_name == 'openidadminpanel', + 'nav_openid_admin_panel' + ); + } + + return true; + } + /** * Add our version information to output * diff --git a/plugins/OpenID/extlib/README b/plugins/OpenID/extlib/README new file mode 100644 index 0000000000..1fe80d79b6 --- /dev/null +++ b/plugins/OpenID/extlib/README @@ -0,0 +1,6 @@ +team-extension.php + Support for Launchpad's OpenID Teams extension + Maintainer: Canonical + Source: https://code.edge.launchpad.net/wordpress-teams-integration + r27 2010-04-27 + License: AGPLv3 diff --git a/plugins/OpenID/extlib/teams-extension.php b/plugins/OpenID/extlib/teams-extension.php new file mode 100644 index 0000000000..451f2fb194 --- /dev/null +++ b/plugins/OpenID/extlib/teams-extension.php @@ -0,0 +1,175 @@ +. + */ + +/** + * Provides an example OpenID extension to query user team/group membership + * + * This code is based on code supplied with the openid library for simple + * registration data. + */ + +/** + * Require the Message implementation. + */ +require_once 'Auth/OpenID/Message.php'; +require_once 'Auth/OpenID/Extension.php'; + +/** + * The team/group extension base class + */ +class Auth_OpenID_TeamsExtension extends Auth_OpenID_Extension { + var $ns_uri = 'http://ns.launchpad.net/2007/openid-teams'; + var $ns_alias = 'lp'; + var $request_field = 'query_membership'; + var $response_field = 'is_member'; + + /** + * Get the string arguments that should be added to an OpenID + * message for this extension. + */ + function getExtensionArgs() { + $args = array(); + + if ($this->_teams) { + $args[$this->request_field] = implode(',', $this->_teams); + } + + return $args; + } + + /** + * Add the arguments from this extension to the provided message. + * + * Returns the message with the extension arguments added. + */ + function toMessage(&$message) { + if ($message->namespaces->addAlias($this->ns_uri, $this->ns_alias) === null) { + if ($message->namespaces->getAlias($this->ns_uri) != $this->ns_alias) { + return null; + } + } + + $message->updateArgs($this->ns_uri, $this->getExtensionArgs()); + return $message; + } + + /** + * Extract the team/group namespace URI from the given OpenID message. + * Handles OpenID 1 and 2. + * + * $message: The OpenID message from which to parse team/group data. + * This may be a request or response message. + * + * Returns the sreg namespace URI for the supplied message. + * + * @access private + */ + function _getExtensionNS(&$message) { + $alias = null; + $found_ns_uri = null; + + // See if there exists an alias for the namespace + $alias = $message->namespaces->getAlias($this->ns_uri); + + if ($alias !== null) { + $found_ns_uri = $this->ns_uri; + } + + if ($alias === null) { + // There is no alias for this extension, so try to add one. + $found_ns_uri = Auth_OpenID_TYPE_1_0; + + if ($message->namespaces->addAlias($this->ns_uri, $this->ns_alias) === null) { + // An alias for the string 'lp' already exists, but + // it's defined for something other than team/group membership + return null; + } + } + + return $found_ns_uri; + } +} + +/** + * The team/group extension request class + */ +class Auth_OpenID_TeamsRequest extends Auth_OpenID_TeamsExtension { + function __init($teams) { + if (!is_array($teams)) { + if (!empty($teams)) { + $teams = explode(',', $teams); + } else { + $teams = Array(); + } + } + + $this->_teams = $teams; + } + + function Auth_OpenID_TeamsRequest($teams) { + $this->__init($teams); + } +} + +/** + * The team/group extension response class + */ +class Auth_OpenID_TeamsResponse extends Auth_OpenID_TeamsExtension { + var $_teams = array(); + + function __init(&$resp, $signed_only=true) { + $this->ns_uri = $this->_getExtensionNS($resp->message); + + if ($signed_only) { + $args = $resp->getSignedNS($this->ns_uri); + } else { + $args = $resp->message->getArgs($this->ns_uri); + } + + if ($args === null) { + return null; + } + + // An OpenID 2.0 response will handle the namespaces + if (in_array($this->response_field, array_keys($args)) && !empty($args[$this->response_field])) { + $this->_teams = explode(',', $args[$this->response_field]); + } + + // Piggybacking on a 1.x request, however, won't so the field name will + // be different + elseif (in_array($this->ns_alias.'.'.$this->response_field, array_keys($args)) && !empty($args[$this->ns_alias.'.'.$this->response_field])) { + $this->_teams = explode(',', $args[$this->ns_alias.'.'.$this->response_field]); + } + } + + function Auth_OpenID_TeamsResponse(&$resp, $signed_only=true) { + $this->__init($resp, $signed_only); + } + + /** + * Get the array of teams the user is a member of + * + * @return array + */ + function getTeams() { + return $this->_teams; + } +} + +?> diff --git a/plugins/OpenID/finishaddopenid.php b/plugins/OpenID/finishaddopenid.php index 991e6584ee..df1763a52c 100644 --- a/plugins/OpenID/finishaddopenid.php +++ b/plugins/OpenID/finishaddopenid.php @@ -103,6 +103,12 @@ class FinishaddopenidAction extends Action $sreg = $sreg_resp->contents(); } + // Launchpad teams extension + if (!oid_check_teams($response)) { + $this->message(_m('OpenID authentication aborted: you are not allowed to login to this site.')); + return; + } + $cur = common_current_user(); $other = oid_get_user($canonical); diff --git a/plugins/OpenID/finishopenidlogin.php b/plugins/OpenID/finishopenidlogin.php index 32b092a0bd..57723ff97f 100644 --- a/plugins/OpenID/finishopenidlogin.php +++ b/plugins/OpenID/finishopenidlogin.php @@ -177,6 +177,12 @@ class FinishopenidloginAction extends Action $sreg = $sreg_resp->contents(); } + // Launchpad teams extension + if (!oid_check_teams($response)) { + $this->message(_m('OpenID authentication aborted: you are not allowed to login to this site.')); + return; + } + $user = oid_get_user($canonical); if ($user) { diff --git a/plugins/OpenID/openid.php b/plugins/OpenID/openid.php index 4ec336e1c3..5ee9343d28 100644 --- a/plugins/OpenID/openid.php +++ b/plugins/OpenID/openid.php @@ -164,6 +164,15 @@ function oid_authenticate($openid_url, $returnto, $immediate=false) $auth_request->addExtension($sreg_request); } + $requiredTeam = common_config('openid', 'required_team'); + if ($requiredTeam) { + // LaunchPad OpenID extension + $team_request = new Auth_OpenID_TeamsRequest(array($requiredTeam)); + if ($team_request) { + $auth_request->addExtension($team_request); + } + } + $trust_root = common_root_url(true); $process_url = common_local_url($returnto); @@ -286,6 +295,33 @@ function oid_assert_allowed($url) return; } +/** + * Check the teams available in the given OpenID response + * Using Launchpad's OpenID teams extension + * + * @return boolean whether this user is acceptable + */ +function oid_check_teams($response) +{ + $requiredTeam = common_config('openid', 'required_team'); + if ($requiredTeam) { + $team_resp = new Auth_OpenID_TeamsResponse($response); + if ($team_resp) { + $teams = $team_resp->getTeams(); + } else { + $teams = array(); + } + + $match = in_array($requiredTeam, $teams); + $is = $match ? 'is' : 'is not'; + common_log(LOG_DEBUG, "Remote user $is in required team $requiredTeam: [" . implode(', ', $teams) . "]"); + + return $match; + } + + return true; +} + class AutosubmitAction extends Action { var $form_html = null; diff --git a/plugins/OpenID/openidadminpanel.php b/plugins/OpenID/openidadminpanel.php new file mode 100644 index 0000000000..0633063662 --- /dev/null +++ b/plugins/OpenID/openidadminpanel.php @@ -0,0 +1,270 @@ +. + * + * @category Settings + * @package StatusNet + * @author Zach Copley + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Administer global OpenID settings + * + * @category Admin + * @package StatusNet + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class OpenidadminpanelAction extends AdminPanelAction +{ + /** + * Returns the page title + * + * @return string page title + */ + + function title() + { + return _m('OpenID'); + } + + /** + * Instructions for using this form. + * + * @return string instructions + */ + + function getInstructions() + { + return _m('OpenID settings'); + } + + /** + * Show the OpenID admin panel form + * + * @return void + */ + + function showForm() + { + $form = new OpenIDAdminPanelForm($this); + $form->show(); + return; + } + + /** + * Save settings from the form + * + * @return void + */ + + function saveSettings() + { + static $settings = array( + 'openid' => array('trusted_provider', 'required_team') + ); + + static $booleans = array( + 'site' => array('openidonly') + ); + + $values = array(); + + foreach ($settings as $section => $parts) { + foreach ($parts as $setting) { + $values[$section][$setting] + = $this->trimmed($setting); + } + } + + foreach ($booleans as $section => $parts) { + foreach ($parts as $setting) { + $values[$section][$setting] + = ($this->boolean($setting)) ? 1 : 0; + } + } + + // This throws an exception on validation errors + + $this->validate($values); + + // assert(all values are valid); + + $config = new Config(); + + $config->query('BEGIN'); + + foreach ($settings as $section => $parts) { + foreach ($parts as $setting) { + Config::save($section, $setting, $values[$section][$setting]); + } + } + + foreach ($booleans as $section => $parts) { + foreach ($parts as $setting) { + Config::save($section, $setting, $values[$section][$setting]); + } + } + + $config->query('COMMIT'); + + return; + } + + function validate(&$values) + { + // Validate consumer key and secret (can't be too long) + + if (mb_strlen($values['openid']['trusted_provider']) > 255) { + $this->clientError( + _m("Invalid provider URL. Max length is 255 characters.") + ); + } + + if (mb_strlen($values['openid']['required_team']) > 255) { + $this->clientError( + _m("Invalid team name. Max length is 255 characters.") + ); + } + } +} + +class OpenIDAdminPanelForm extends AdminForm +{ + /** + * ID of the form + * + * @return int ID of the form + */ + + function id() + { + return 'openidadminpanel'; + } + + /** + * class of the form + * + * @return string class of the form + */ + + function formClass() + { + return 'form_settings'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return common_local_url('openidadminpanel'); + } + + /** + * Data elements of the form + * + * @return void + * + * @todo Some of the options could prevent users from logging in again. + * Make sure that the acting administrator has a valid OpenID matching, + * or more carefully warn folks. + */ + + function formData() + { + $this->out->elementStart( + 'fieldset', + array('id' => 'settings_openid') + ); + $this->out->element('legend', null, _m('Trusted provider')); + $this->out->element('p', 'form_guide', + _m('By default, users are allowed to authenticate with any OpenID provider. ' . + 'If you are using your own OpenID service for shared sign-in, ' . + 'you can restrict access to only your own users here.')); + $this->out->elementStart('ul', 'form_data'); + + $this->li(); + $this->input( + 'trusted_provider', + _m('Provider URL'), + _m('All OpenID logins will be sent to this URL; other providers may not be used.'), + 'openid' + ); + $this->unli(); + + $this->li(); + $this->input( + 'required_team', + _m('Required team'), + _m('Only allow logins from users in the given team (Launchpad extension).'), + 'openid' + ); + $this->unli(); + + $this->out->elementEnd('ul'); + $this->out->elementEnd('fieldset'); + + $this->out->elementStart( + 'fieldset', + array('id' => 'settings_openid-options') + ); + $this->out->element('legend', null, _m('Options')); + + $this->out->elementStart('ul', 'form_data'); + + $this->li(); + + $this->out->checkbox( + 'openidonly', _m('Enable OpenID-only mode'), + (bool) $this->value('openidonly', 'site'), + _m('Require all users to login via OpenID. WARNING: disables password authentication for all users!'), + 'true' + ); + $this->unli(); + + $this->out->elementEnd('ul'); + + $this->out->elementEnd('fieldset'); + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + $this->out->submit('submit', _('Save'), 'submit', null, _m('Save OpenID settings')); + } +} diff --git a/plugins/OpenID/openidlogin.php b/plugins/OpenID/openidlogin.php index 2a743672cf..8c559c9346 100644 --- a/plugins/OpenID/openidlogin.php +++ b/plugins/OpenID/openidlogin.php @@ -29,7 +29,12 @@ class OpenidloginAction extends Action if (common_is_real_login()) { $this->clientError(_m('Already logged in.')); } else if ($_SERVER['REQUEST_METHOD'] == 'POST') { - $openid_url = $this->trimmed('openid_url'); + $provider = common_config('openid', 'trusted_provider'); + if ($provider) { + $openid_url = $provider; + } else { + $openid_url = $this->trimmed('openid_url'); + } oid_assert_allowed($openid_url); @@ -116,9 +121,18 @@ class OpenidloginAction extends Action $this->elementStart('ul', 'form_data'); $this->elementStart('li'); - $this->input('openid_url', _m('OpenID URL'), - $this->openid_url, - _m('Your OpenID URL')); + $provider = common_config('openid', 'trusted_provider'); + if ($provider) { + $this->element('label', array(), _m('OpenID provider')); + $this->element('span', array(), $provider); + $this->element('p', 'form_guide', + _m('You will be sent to the provider\'s site for authentication.')); + $this->hidden('openid_url', $provider); + } else { + $this->input('openid_url', _m('OpenID URL'), + $this->openid_url, + _m('Your OpenID URL')); + } $this->elementEnd('li'); $this->elementStart('li', array('id' => 'settings_rememberme')); $this->checkbox('rememberme', _m('Remember me'), false, diff --git a/plugins/OpenID/openidsettings.php b/plugins/OpenID/openidsettings.php index 16142cf48c..505e7d0ee3 100644 --- a/plugins/OpenID/openidsettings.php +++ b/plugins/OpenID/openidsettings.php @@ -90,34 +90,36 @@ class OpenidsettingsAction extends AccountSettingsAction { $user = common_current_user(); - $this->elementStart('form', array('method' => 'post', - 'id' => 'form_settings_openid_add', - 'class' => 'form_settings', - 'action' => - common_local_url('openidsettings'))); - $this->elementStart('fieldset', array('id' => 'settings_openid_add')); - $this->element('legend', null, _m('Add OpenID')); - $this->hidden('token', common_session_token()); - $this->element('p', 'form_guide', - _m('If you want to add an OpenID to your account, ' . - 'enter it in the box below and click "Add".')); - $this->elementStart('ul', 'form_data'); - $this->elementStart('li'); - $this->element('label', array('for' => 'openid_url'), - _m('OpenID URL')); - $this->element('input', array('name' => 'openid_url', - 'type' => 'text', - 'id' => 'openid_url')); - $this->elementEnd('li'); - $this->elementEnd('ul'); - $this->element('input', array('type' => 'submit', - 'id' => 'settings_openid_add_action-submit', - 'name' => 'add', - 'class' => 'submit', - 'value' => _m('Add'))); - $this->elementEnd('fieldset'); - $this->elementEnd('form'); - + if (!common_config('openid', 'trusted_provider')) { + $this->elementStart('form', array('method' => 'post', + 'id' => 'form_settings_openid_add', + 'class' => 'form_settings', + 'action' => + common_local_url('openidsettings'))); + $this->elementStart('fieldset', array('id' => 'settings_openid_add')); + + $this->element('legend', null, _m('Add OpenID')); + $this->hidden('token', common_session_token()); + $this->element('p', 'form_guide', + _m('If you want to add an OpenID to your account, ' . + 'enter it in the box below and click "Add".')); + $this->elementStart('ul', 'form_data'); + $this->elementStart('li'); + $this->element('label', array('for' => 'openid_url'), + _m('OpenID URL')); + $this->element('input', array('name' => 'openid_url', + 'type' => 'text', + 'id' => 'openid_url')); + $this->elementEnd('li'); + $this->elementEnd('ul'); + $this->element('input', array('type' => 'submit', + 'id' => 'settings_openid_add_action-submit', + 'name' => 'add', + 'class' => 'submit', + 'value' => _m('Add'))); + $this->elementEnd('fieldset'); + $this->elementEnd('form'); + } $oid = new User_openid(); $oid->user_id = $user->id; @@ -234,10 +236,14 @@ class OpenidsettingsAction extends AccountSettingsAction } if ($this->arg('add')) { - $result = oid_authenticate($this->trimmed('openid_url'), - 'finishaddopenid'); - if (is_string($result)) { // error message - $this->showForm($result); + if (common_config('openid', 'trusted_provider')) { + $this->showForm(_m("Can't add new providers.")); + } else { + $result = oid_authenticate($this->trimmed('openid_url'), + 'finishaddopenid'); + if (is_string($result)) { // error message + $this->showForm($result); + } } } else if ($this->arg('remove')) { $this->removeOpenid();