diff --git a/EVENTS.txt b/EVENTS.txt index ced130f5f7..97b7de299f 100644 --- a/EVENTS.txt +++ b/EVENTS.txt @@ -491,11 +491,13 @@ EndCheckPassword: After checking a username/password pair - $password: The password that was checked - $authenticatedUser: User object if credentials match a user, else null. -ChangePassword: Handle a password change request +StartChangePassword: Before changing a password - $nickname: user's nickname - $oldpassword: the user's old password - $newpassword: the desired new password -- &$errormsg: set this to an error message if the password could not be changed. If the password was changed, leave this as false + +EndChangePassword: After changing a password +- $nickname: user's nickname CanUserChangeField: Determines if a user is allowed to change a specific profile field - $nickname: nickname of the user who would like to know which of their profile fields are mutable diff --git a/actions/passwordsettings.php b/actions/passwordsettings.php index 024f1287f2..9e79501e2d 100644 --- a/actions/passwordsettings.php +++ b/actions/passwordsettings.php @@ -58,19 +58,6 @@ class PasswordsettingsAction extends AccountSettingsAction return _('Change password'); } - function prepare($args){ - parent::prepare($args); - - $user = common_current_user(); - - Event::handle('CanUserChangeField', array($user->nickname, 'password')); - - if(! $fields['password']){ - //user is not allowed to change his password - $this->clientError(_('You are not allowed to change your password')); - } - } - /** * Instructions for use * @@ -182,8 +169,8 @@ class PasswordsettingsAction extends AccountSettingsAction $oldpassword = null; } - $errormsg = false; - if(! Event::handle('ChangePassword', array($user->nickname, $oldpassword, $newpassword, &$errormsg))){ + $success = false; + if(! Event::handle('StartChangePassword', array($user->nickname, $oldpassword, $newpassword))){ //no handler changed the password, so change the password internally $original = clone($user); @@ -199,11 +186,9 @@ class PasswordsettingsAction extends AccountSettingsAction $this->serverError(_('Can\'t save new password.')); return; } + Event::handle('EndChangePassword', array($nickname)); } - if($errormsg === false) - $this->showForm(_('Password saved.'), true); - else - $this->showForm($errormsg); + $this->showForm(_('Password saved.'), true); } } diff --git a/plugins/Auth/AuthPlugin.php b/plugins/Auth/AuthPlugin.php new file mode 100644 index 0000000000..71e7ae4fbc --- /dev/null +++ b/plugins/Auth/AuthPlugin.php @@ -0,0 +1,145 @@ +. + * + * @category Plugin + * @package StatusNet + * @author Craig Andrews + * @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') && !defined('LACONICA')) { + exit(1); +} + +/** + * Superclass for plugins that do authentication + * + * @category Plugin + * @package StatusNet + * @author Craig Andrews + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +abstract class AuthPlugin extends Plugin +{ + //is this plugin authoritative for authentication? + protected $authn_authoritative = false; + + //should accounts be automatically created after a successful login attempt? + protected $autoregistration = false; + + //------------Auth plugin should implement some (or all) of these methods------------\\ + /** + * Check if a nickname/password combination is valid + * @param nickname + * @param password + * @return boolean true if the credentials are valid, false if they are invalid. + */ + function checkPassword($nickname, $password) + { + return false; + } + + /** + * Automatically register a user when they attempt to login with valid credentials. + * User::register($data) is a very useful method for this implementation + * @param nickname + * @return boolean true if the user was created, false if autoregistration is not allowed, null if this plugin is not responsible for this nickname + */ + function autoRegister($nickname) + { + return null; + } + + /** + * Change a user's password + * The old password has been verified to be valid by this plugin before this call is made + * @param nickname + * @param oldpassword + * @param newpassword + * @return boolean true if the password was changed, false if password changing failed for some reason, null if this plugin is not responsible for this nickname + */ + function changePassword($nickname,$oldpassword,$newpassword) + { + return null; + } + + /** + * Can a user change this field in his own profile? + * @param nickname + * @param field + * @return boolean true if the field can be changed, false if not allowed to change it, null if this plugin is not responsible for this nickname + */ + function canUserChangeField($nickname, $field) + { + return null; + } + + //------------Below are the methods that connect StatusNet to the implementing Auth plugin------------\\ + function __construct() + { + parent::__construct(); + } + + function StartCheckPassword($nickname, $password, &$authenticatedUser){ + $authenticated = $this->checkPassword($nickname, $password); + if($authenticated){ + $authenticatedUser = User::staticGet('nickname', $nickname); + if(!$authenticatedUser && $this->autoregistration){ + if($this->autoregister($nickname)){ + $authenticatedUser = User::staticGet('nickname', $nickname); + } + } + return false; + }else{ + if($this->authn_authoritative){ + return false; + } + } + //we're not authoritative, so let other handlers try + } + + function onStartChangePassword($nickname,$oldpassword,$newpassword) + { + $authenticated = $this->checkPassword($nickname, $oldpassword); + if($authenticated){ + $result = $this->changePassword($nickname,$oldpassword,$newpassword); + if($result){ + //stop handling of other handlers, because what was requested was done + return false; + }else{ + throw new Exception(_('Password changing failed')); + } + }else{ + if($this->authn_authoritative){ + //since we're authoritative, no other plugin could do this + throw new Exception(_('Password changing failed')); + }else{ + //let another handler try + return null; + } + } + + } +} + diff --git a/plugins/Ldap/LdapPlugin.php b/plugins/Ldap/LdapPlugin.php index 3795ffd7f1..8a416bccc7 100644 --- a/plugins/Ldap/LdapPlugin.php +++ b/plugins/Ldap/LdapPlugin.php @@ -31,38 +31,42 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once INSTALLDIR.'/plugins/Ldap/ldap.php'; +require_once INSTALLDIR.'/plugins/Auth/AuthPlugin.php'; +require_once 'Net/LDAP2.php'; -class LdapPlugin extends Plugin +class LdapPlugin extends AuthPlugin { - private $config = array(); function __construct() { parent::__construct(); } + + //---interface implementation---// - function onCheckPassword($nickname, $password, &$authenticated) + function checkPassword($nickname, $password) { - if(ldap_check_password($nickname, $password)){ - $authenticated = true; - //stop handling of other events, because we have an answer + $ldap = $this->ldap_get_connection(); + if(!$ldap){ return false; } - if(common_config('ldap','authoritative')){ - //a false return stops handler processing + $entry = $this->ldap_get_user($nickname); + if(!$entry){ return false; + }else{ + $config = $this->ldap_get_config(); + $config['binddn']=$entry->dn(); + $config['bindpw']=$password; + if($this->ldap_get_connection($config)){ + return true; + }else{ + return false; + } } } - function onAutoRegister($nickname) + function autoRegister($nickname) { - $user = User::staticGet('nickname', $nickname); - if (! is_null($user) && $user !== false) { - common_log(LOG_WARNING, "An attempt was made to autoregister an existing user with nickname: $nickname"); - return; - } - $attributes=array(); $config_attributes = array('nickname','email','fullname','homepage','location'); foreach($config_attributes as $config_attribute){ @@ -71,7 +75,7 @@ class LdapPlugin extends Plugin array_push($attributes,$value); } } - $entry = ldap_get_user($nickname,$attributes); + $entry = $this->ldap_get_user($nickname,$attributes); if($entry){ $registration_data = array(); foreach($config_attributes as $config_attribute){ @@ -89,21 +93,22 @@ class LdapPlugin extends Plugin //set the database saved password to a random string. $registration_data['password']=common_good_rand(16); $user = User::register($registration_data); - //prevent other handlers from running, as we have registered the user - return false; + return true; + }else{ + //user isn't in ldap, so we cannot register him + return null; } } - function onChangePassword($nickname,$oldpassword,$newpassword,&$errormsg) + function changePassword($nickname,$oldpassword,$newpassword) { //TODO implement this - $errormsg = _('Sorry, changing LDAP passwords is not supported at this time'); + throw new Exception(_('Sorry, changing LDAP passwords is not supported at this time')); - //return false, indicating that the event has been handled return false; } - function onCanUserChangeField($nickname, $field) + function canUserChangeField($nickname, $field) { switch($field) { @@ -113,4 +118,67 @@ class LdapPlugin extends Plugin return false; } } + + //---utility functions---// + function ldap_get_config(){ + $config = array(); + $keys = array('host','port','version','starttls','binddn','bindpw','basedn','options','filter','scope'); + foreach($keys as $key){ + $value = $this->$key; + if($value!==false){ + $config[$key]=$value; + } + } + return $config; + } + + function ldap_get_connection($config = null){ + if($config == null){ + $config = $this->ldap_get_config(); + } + + //cannot use Net_LDAP2::connect() as StatusNet uses + //PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'handleError'); + //PEAR handling can be overridden on instance objects, so we do that. + $ldap = new Net_LDAP2($config); + $ldap->setErrorHandling(PEAR_ERROR_RETURN); + $err=$ldap->bind(); + if (Net_LDAP2::isError($err)) { + common_log(LOG_WARNING, 'Could not connect to LDAP server: '.$err->getMessage()); + return false; + } + return $ldap; + } + + /** + * get an LDAP entry for a user with a given username + * + * @param string $username + * $param array $attributes LDAP attributes to retrieve + * @return string DN + */ + function ldap_get_user($username,$attributes=array()){ + $ldap = $this->ldap_get_connection(); + $filter = Net_LDAP2_Filter::create(common_config('ldap','nickname_attribute'), 'equals', $username); + $options = array( + 'scope' => 'sub', + 'attributes' => $attributes + ); + $search = $ldap->search(null,$filter,$options); + + if (PEAR::isError($search)) { + common_log(LOG_WARNING, 'Error while getting DN for user: '.$search->getMessage()); + return false; + } + + if($search->count()==0){ + return false; + }else if($search->count()==1){ + $entry = $search->shiftEntry(); + return $entry; + }else{ + common_log(LOG_WARNING, 'Found ' . $search->count() . ' ldap user with the username: ' . $username); + return false; + } + } } diff --git a/plugins/Ldap/README b/plugins/Ldap/README index 617738e0ba..1b6e3e75a9 100644 --- a/plugins/Ldap/README +++ b/plugins/Ldap/README @@ -2,22 +2,46 @@ The LDAP plugin allows for StatusNet to handle authentication, authorization, an Installation ============ -Add configuration entries to config.php. These entries are: +add "addPlugin('ldap', array('setting'=>'value', 'setting2'=>'value2', ...);" to the bottom of your config.php -The following are documented at http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php -$config['ldap']['binddn'] -$config['ldap']['bindpw'] -$config['ldap']['basedn'] -$config['ldap']['host'] -$config['ldap']['nickname_attribute'] Set this to the name of the ldap attribute that holds the username. For example, on Microsoft's Active Directory, this should be set to 'sAMAccountName' -$config['ldap']['nickname_email'] Set this to the name of the ldap attribute that holds the user's email address. For example, on Microsoft's Active Directory, this should be set to 'mail' -$config['ldap']['nickname_fullname'] Set this to the name of the ldap attribute that holds the user's full name. For example, on Microsoft's Active Directory, this should be set to 'displayName' -$config['ldap']['nickname_homepage'] Set this to the name of the ldap attribute that holds the the url of the user's home page. -$config['ldap']['nickname_location'] Set this to the name of the ldap attribute that holds the user's location. -$config['ldap']['authoritative'] Set to true if LDAP's responses are authoritative (meaning if LDAP fails, do check the any other plugins or the internal password database) -$config['ldap']['autoregister'] Set to true if users should be automatically created when they attempt to login +Settings +======== +authn_authoritative: Set to true if LDAP's responses are authoritative (meaning if LDAP fails, do check the any other plugins or the internal password database). +autoregistration: Set to true if users should be automatically created when they attempt to login. -Finally, add "addPlugin('ldap');" to the bottom of your config.php +host*: LDAP server name to connect to. You can provide several hosts in an array in which case the hosts are tried from left to right.. See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php +port: Port on the server. See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php +version: LDAP version. See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php +starttls: TLS is started after connecting. See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php +binddn: The distinguished name to bind as (username). See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php +bindpw: Password for the binddn. See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php +basedn*: LDAP base name (root directory). See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php +options: See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php +filter: Default search filter. See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php +scope: Default search scope. See http://pear.php.net/manual/en/package.networking.net-ldap2.connecting.php +attributes: an array with the key being the StatusNet user attribute name, and the value the LDAP attribute name + nickname* + email + fullname + homepage + location + +* required + +Example +======= +Here's an example of an LDAP plugin configuration that connects to Microsoft Active Directory. + +addPlugin('ldap', array( + 'binddn'=>'username', + 'bindpw'=>'password', + 'basedn'=>'OU=Users,OU=StatusNet,OU=US,DC=americas,DC=global,DC=loc', + 'host'=>array('server1', 'server2'), + 'attributes'=>array( + 'nickname'=>'sAMAccountName', + 'email'=>'mail', + 'fullname'=>'displayName') +)); diff --git a/plugins/Ldap/ldap.php b/plugins/Ldap/ldap.php deleted file mode 100644 index d92a058fb9..0000000000 --- a/plugins/Ldap/ldap.php +++ /dev/null @@ -1,108 +0,0 @@ -. - */ - -if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } - -require_once 'Net/LDAP2.php'; - -function ldap_get_config(){ - static $config = null; - if($config == null){ - $config = array(); - $keys = array('host','port','version','starttls','binddn','bindpw','basedn','options','scope'); - foreach($keys as $key){ - $value = common_config('ldap', $key); - if($value!==false){ - $config[$key]=$value; - } - } - } - return $config; -} - -function ldap_get_connection($config = null){ - if($config == null){ - $config = ldap_get_config(); - } - - //cannot use Net_LDAP2::connect() as StatusNet uses - //PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'handleError'); - //PEAR handling can be overridden on instance objects, so we do that. - $ldap = new Net_LDAP2($config); - $ldap->setErrorHandling(PEAR_ERROR_RETURN); - $err=$ldap->bind(); - if (Net_LDAP2::isError($err)) { - common_log(LOG_WARNING, 'Could not connect to LDAP server: '.$err->getMessage()); - return false; - } - return $ldap; -} - -function ldap_check_password($username, $password){ - $ldap = ldap_get_connection(); - if(!$ldap){ - return false; - } - $entry = ldap_get_user($username); - if(!$entry){ - return false; - }else{ - $config = ldap_get_config(); - $config['binddn']=$entry->dn(); - $config['bindpw']=$password; - if(ldap_get_connection($config)){ - return true; - }else{ - return false; - } - } -} - -/** - * get an LDAP entry for a user with a given username - * - * @param string $username - * $param array $attributes LDAP attributes to retrieve - * @return string DN - */ -function ldap_get_user($username,$attributes=array()){ - $ldap = ldap_get_connection(); - $filter = Net_LDAP2_Filter::create(common_config('ldap','nickname_attribute'), 'equals', $username); - $options = array( - 'scope' => 'sub', - 'attributes' => $attributes - ); - $search = $ldap->search(null,$filter,$options); - - if (PEAR::isError($search)) { - common_log(LOG_WARNING, 'Error while getting DN for user: '.$search->getMessage()); - return false; - } - - if($search->count()==0){ - return false; - }else if($search->count()==1){ - $entry = $search->shiftEntry(); - return $entry; - }else{ - common_log(LOG_WARNING, 'Found ' . $search->count() . ' ldap user with the username: ' . $username); - return false; - } -} -