diff --git a/EVENTS.txt b/EVENTS.txt index c788a9215f..34a222e8f3 100644 --- a/EVENTS.txt +++ b/EVENTS.txt @@ -535,6 +535,28 @@ StartChangePassword: Before changing a password EndChangePassword: After changing a password - $user: user +StartSetUser: Before setting the currently logged in user +- $user: user + +EndSetUser: After setting the currently logged in user +- $user: user + +StartSetApiUser: Before setting the current API user +- $user: user + +EndSetApiUser: After setting the current API user +- $user: user + +StartHasRole: Before determing if the a profile has a given role +- $profile: profile in question +- $name: name of the role in question +- &$has_role: does this profile have the named role? + +EndHasRole: Before determing if the a profile has a given role +- $profile: profile in question +- $name: name of the role in question +- $has_role: does this profile have the named role? + UserDeleteRelated: Specify additional tables to delete entries from when deleting users - $user: User object - &$related: array of DB_DataObject class names to delete entries on matching user_id. diff --git a/classes/Profile.php b/classes/Profile.php index 1b9cdb52f9..4b2e090064 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -594,9 +594,14 @@ class Profile extends Memcached_DataObject function hasRole($name) { - $role = Profile_role::pkeyGet(array('profile_id' => $this->id, - 'role' => $name)); - return (!empty($role)); + $has_role = false; + if (Event::handle('StartHasRole', array($this, $name, &$has_role))) { + $role = Profile_role::pkeyGet(array('profile_id' => $this->id, + 'role' => $name)); + $has_role = !empty($role); + Event::handle('EndHasRole', array($this, $name, $has_role)); + } + return $has_role; } function grantRole($name) diff --git a/lib/apiauth.php b/lib/apiauth.php index 2f2e44a264..0d1613d381 100644 --- a/lib/apiauth.php +++ b/lib/apiauth.php @@ -110,7 +110,11 @@ class ApiAuthAction extends ApiAction } else { $nickname = $this->auth_user; $password = $this->auth_pw; - $this->auth_user = common_check_user($nickname, $password); + $user = common_check_user($nickname, $password); + if (Event::handle('StartSetApiUser', array(&$user))) { + $this->auth_user = $user; + Event::handle('EndSetApiUser', array($user)); + } if (empty($this->auth_user)) { diff --git a/lib/util.php b/lib/util.php index 68f3520db5..5bf4f6091b 100644 --- a/lib/util.php +++ b/lib/util.php @@ -196,10 +196,15 @@ function common_set_user($user) } if ($user) { - common_ensure_session(); - $_SESSION['userid'] = $user->id; - $_cur = $user; - return $_cur; + if (Event::handle('StartSetUser', array(&$user))) { + if($user){ + common_ensure_session(); + $_SESSION['userid'] = $user->id; + $_cur = $user; + Event::handle('EndSetUser', array($user)); + return $_cur; + } + } } return false; } diff --git a/plugins/Authorization/AuthorizationPlugin.php b/plugins/Authorization/AuthorizationPlugin.php new file mode 100644 index 0000000000..be39aedd21 --- /dev/null +++ b/plugins/Authorization/AuthorizationPlugin.php @@ -0,0 +1,112 @@ +. + * + * @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 authorization + * + * @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 AuthorizationPlugin extends Plugin +{ + //is this plugin authoritative for authorization? + public $authoritative = false; + + //------------Auth plugin should implement some (or all) of these methods------------\\ + + /** + * Is a user allowed to log in? + * @param user + * @return boolean true if the user is allowed to login, false if explicitly not allowed to login, null if we don't explicitly allow or deny login + */ + function loginAllowed($user) { + return null; + } + + /** + * Does a profile grant the user a named role? + * @param profile + * @return boolean true if the profile has the role, false if not + */ + function hasRole($profile, $name) { + return false; + } + + //------------Below are the methods that connect StatusNet to the implementing Auth plugin------------\\ + function onInitializePlugin(){ + + } + + function onStartSetUser(&$user) { + $loginAllowed = $this->loginAllowed($user); + if($loginAllowed === true){ + if($this->authoritative) { + return false; + }else{ + return; + } + }else if($loginAllowed === false){ + $user = null; + return false; + }else{ + if($this->authoritative) { + $user = null; + return false; + }else{ + return; + } + } + } + + function onStartSetApiUser(&$user) { + return onStartSetUser(&$user); + } + + function onStartHasRole($profile, $name, &$has_role) { + if($this->hasRole($profile, $name)){ + $has_role = true; + return false; + }else{ + if($this->authoritative) { + $has_role = false; + return false; + }else{ + return; + } + } + } +} + diff --git a/plugins/LdapAuthentication/LdapAuthenticationPlugin.php b/plugins/LdapAuthentication/LdapAuthenticationPlugin.php index 664529497c..555dabf78d 100644 --- a/plugins/LdapAuthentication/LdapAuthenticationPlugin.php +++ b/plugins/LdapAuthentication/LdapAuthenticationPlugin.php @@ -2,7 +2,7 @@ /** * StatusNet, the distributed open-source microblogging tool * - * Plugin to enable LDAP Authentication and Authorization + * Plugin to enable LDAP Authentication * * PHP version 5 * @@ -65,6 +65,7 @@ class LdapAuthenticationPlugin extends AuthenticationPlugin } if($this->password_changeable && (! isset($this->attributes['password']) || !isset($this->password_encoding))){ throw new Exception("if password_changeable is set, the password attribute and password_encoding must also be specified"); + } } //---interface implementation---// diff --git a/plugins/LdapAuthorization/LdapAuthorizationPlugin.php b/plugins/LdapAuthorization/LdapAuthorizationPlugin.php new file mode 100644 index 0000000000..20bbd25625 --- /dev/null +++ b/plugins/LdapAuthorization/LdapAuthorizationPlugin.php @@ -0,0 +1,129 @@ +. + * + * @category Plugin + * @package StatusNet + * @author Craig Andrews + * @copyright 2009 Craig Andrews http://candrews.integralblue.com + * @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); +} + +require_once INSTALLDIR.'/plugins/Authorization/AuthorizationPlugin.php'; +require_once 'Net/LDAP2.php'; + +class LdapAuthorizationPlugin extends AuthorizationPlugin +{ + public $host=null; + public $port=null; + public $version=null; + public $starttls=null; + public $binddn=null; + public $bindpw=null; + public $basedn=null; + public $options=null; + public $filter=null; + public $scope=null; + public $provider_name = null; + public $uniqueMember_attribute = null; + public $roles_to_groups = null; + + function onInitializePlugin(){ + parent::onInitializePlugin(); + if(!isset($this->host)){ + throw new Exception("must specify a host"); + } + if(!isset($this->basedn)){ + throw new Exception("must specify a basedn"); + } + if(!isset($this->provider_name)){ + throw new Exception("provider_name must be set. Use the provider_name from the LDAP Authentication plugin."); + } + if(!isset($this->uniqueMember_attribute)){ + throw new Exception("uniqueMember_attribute must be set."); + } + if(!isset($this->roles_to_groups)){ + throw new Exception("roles_to_groups must be set."); + } + } + + //---interface implementation---// + function loginAllowed($user) { + $user_username = new User_username(); + $user_username->user_id=$user->id; + $user_username->provider_name=$this->provider_name; + if($user_username->find() && $user_username->fetch()){ + $entry = $this->ldap_get_user($user_username->username); + if($entry){ + //if a user exists, we can assume he's allowed to login + return true; + }else{ + return null; + } + }else{ + return null; + } + } + + function hasRole($profile, $name) { + $user_username = new User_username(); + $user_username->user_id=$profile->id; + $user_username->provider_name=$this->provider_name; + if($user_username->find() && $user_username->fetch()){ + $entry = $this->ldap_get_user($user_username->username); + if($entry){ + if(isset($this->roles_to_groups[$name])){ + if(is_array($this->roles_to_groups[$name])){ + foreach($this->roles_to_groups[$name] as $group){ + if($this->isMemberOfGroup($entry->dn(),$group)){ + return true; + } + } + }else{ + if($this->isMemberOfGroup($entry->dn(),$this->roles_to_groups[$name])){ + return true; + } + } + } + } + } + return false; + } + + function isMemberOfGroup($userDn, $groupDn) + { + $ldap = ldap_get_connection(); + $link = $ldap->getLink(); + $r = ldap_compare($link, $groupDn, $this->uniqueMember_attribute, $userDn); + if ($r === true){ + return true; + }else if($r === false){ + return false; + }else{ + common_log(LOG_ERR, ldap_error($r)); + return false; + } + } +} diff --git a/plugins/LdapAuthorization/README b/plugins/LdapAuthorization/README new file mode 100644 index 0000000000..2ca33f653d --- /dev/null +++ b/plugins/LdapAuthorization/README @@ -0,0 +1,84 @@ +The LDAP Authorization plugin allows for StatusNet to handle authorization +through LDAP. + +Installation +============ +add "addPlugin('ldapAuthorization', + array('setting'=>'value', 'setting2'=>'value2', ...);" +to the bottom of your config.php + +You *cannot* use this plugin without the LDAP Authentication plugin + +Settings +======== +provider_name*: name of the LDAP authentication provider that this plugin works with. +authoritative (false): should this plugin be authoritative for + authorization? +uniqueMember_attribute ('uniqueMember')*: the attribute of a group + that lists the DNs of its members +roles_to_groups*: array that maps StatusNet roles to LDAP groups + some StatusNet roles are: moderator, administrator, sandboxed, silenced + +The below settings must be exact copies of the settings used for the + corresponding LDAP Authentication plugin. + +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 + +* required +default values are in (parenthesis) + +Example +======= +Here's an example of an LDAP plugin configuration that connects to + Microsoft Active Directory. + +addPlugin('ldapAuthentication', array( + 'provider_name'=>'Example', + 'authoritative'=>true, + 'autoregistration'=>true, + 'binddn'=>'username', + 'bindpw'=>'password', + 'basedn'=>'OU=Users,OU=StatusNet,OU=US,DC=americas,DC=global,DC=loc', + 'host'=>array('server1', 'server2'), + 'password_encoding'=>'ad', + 'attributes'=>array( + 'username'=>'sAMAccountName', + 'nickname'=>'sAMAccountName', + 'email'=>'mail', + 'fullname'=>'displayName', + 'password'=>'unicodePwd') +)); +addPlugin('ldapAuthorization', array( + 'provider_name'=>'Example', + 'authoritative'=>false, + 'uniqueMember_attribute'=>'uniqueMember', + 'roles_to_groups'=> array( + 'moderator'=>'CN=SN-Moderators,OU=Users,OU=StatusNet,OU=US,DC=americas,DC=global,DC=loc', + 'administrator'=> array('CN=System-Adminstrators,OU=Users,OU=StatusNet,OU=US,DC=americas,DC=global,DC=loc', + 'CN=SN-Administrators,OU=Users,OU=StatusNet,OU=US,DC=americas,DC=global,DC=loc') + ), + 'binddn'=>'username', + 'bindpw'=>'password', + 'basedn'=>'OU=Users,OU=StatusNet,OU=US,DC=americas,DC=global,DC=loc', + 'host'=>array('server1', 'server2') +)); +