Commit 939b5170 authored by redflo's avatar redflo

Rework of LDAP Authentication. Dropped PEAR::Auth_LDAP to get more

information from the directory. Added support for User information and
ldap group replication. Added support for ssl (ldaps), tls and redundant LDAP
server configuration.
parent 94843daa
......@@ -1171,6 +1171,7 @@ installer/schema/20090602_webmail_protocols_tiki.sql -text
installer/schema/20090605_tiki_p_modify_tracker_items_pending_closed_tiki.sql -text
installer/schema/20090617_add_new_table_for_reports_tiki.sql -text
installer/schema/20090623_actionlog_view_article_tiki.sql -text
installer/schema/20090626_change_pear_auth_preferences_to_ldap.sql -text
installer/shell.php -text
installer/tiki-installer.php -text
/jhot.php -text
......@@ -1477,6 +1478,7 @@ lib/ajax/lib/activex_off.js -text
lib/ajax/tiki-ajax.js -text
lib/articles/artlib.php -text
lib/articles/index.php -text
lib/auth/ldap.php -text
lib/bablotron.php -text
lib/ban/banlib.php -text
lib/ban/index.php -text
......@@ -2290,6 +2292,16 @@ lib/pear/Net/DNS/RR/SRV.php -text
lib/pear/Net/DNS/RR/TSIG.php -text
lib/pear/Net/DNS/RR/TXT.php -text
lib/pear/Net/DNS/Resolver.php -text
lib/pear/Net/LDAP2.php -text
lib/pear/Net/LDAP2/Entry.php -text
lib/pear/Net/LDAP2/Filter.php -text
lib/pear/Net/LDAP2/LDIF.php -text
lib/pear/Net/LDAP2/RootDSE.php -text
lib/pear/Net/LDAP2/Schema.php -text
lib/pear/Net/LDAP2/SchemaCache.interface.php -text
lib/pear/Net/LDAP2/Search.php -text
lib/pear/Net/LDAP2/SimpleFileSchemaCache.php -text
lib/pear/Net/LDAP2/Util.php -text
lib/pear/Net/Ping.php -text
lib/pear/Net/Socket.php -text
lib/pear/Net/URL.php -text
......
......@@ -2800,6 +2800,7 @@ CREATE TABLE users_groups (
userChoice char(1) default NULL,
groupDefCat int(12) default 0,
groupTheme varchar(255) default '',
isExternal char(1) default 'n',
PRIMARY KEY (id),
UNIQUE KEY groupName (groupName)
) ENGINE=MyISAM AUTO_INCREMENT=1;
......@@ -3885,4 +3886,4 @@ CREATE TABLE IF NOT EXISTS `tiki_user_reports_cache` (
`data` text COLLATE latin1_general_ci NOT NULL,
`time` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM;
\ No newline at end of file
) ENGINE=MyISAM;
update tiki_preferences set name='ldap_create_user_tiki' where name='auth_create_user_tiki';
update tiki_preferences set name='ldap_create_ldap_tiki' where name='auth_create_auth_tiki';
update tiki_preferences set name='ldap_skip_admin' where name='auth_skip_admin';
update tiki_preferences set name='auth_ldap_host' where name='auth_pear_host';
update tiki_preferences set name='auth_ldap_port' where name='auth_pear_port';
update tiki_preferences set value='ldap' where name='auth_method' and value='auth';
alter table users_groups add column(isExternal char default 'n');
<?php
/*
* Class that adds LDAP Authentication to Tiki and aids Tiki to get User/Group Information
* from a LDAP directory
*/
// class uses Pears Net_LDAP2
require_once ("Net/LDAP2.php");
class TikiLdapLib {
// var to hold a esablished connection
protected $ldaplink = NULL;
// var for ldap configuration parameters
protected $options = array(
'host' => 'localhost',
'port' => 389,
'version' => 3,
'starttls' => false,
'ssl' => false,
'basedn' => '',
'filter' => '(objectClass=*)',
'scope' => 'sub',
'bind_type' => 'default',
'username' => '',
'password' => '',
'userdn' => '',
'useroc' => 'inetOrgPerson',
'userattr' => 'cn',
'fullnameattr' => '',
'emailattr' => 'mail',
'countryattr' => '',
'groupdn' => '',
'groupattr' => 'gid',
'groupoc' => 'groupOfNames',
'groupnameattr' => '',
'groupdescattr' => '',
//neu
'groupmemberattr' => '',
'groupmemberisdn' => true,
'usergroupattr' => '',
'groupgroupattr' => '',
// end neu
'debug' => false
);
protected $logslib=NULL;
// Constructor
public function __construct($options) {
// debug setting
global $logslib;
if(isset($options['debug']) && ($options['debug']===true || $options['debug']=='y' )&& ($logslib instanceof LogsLib)) {
$this->options['debug']=true;
$this->logslib=&$logslib;
}
// Configure the connection
// host can be a list of hostnames. If we use ssl, then we have to add ldaps:// to every hostname
if(isset($options['host']) && !empty($options['host'])) {
$t=preg_split('#[\s,]#',$options['host']);
// print_r($options);
if(isset($options['ssl']) && ($options['ssl']=='y' || $options['ssl']===true)) {
$this->options['host']=array();
foreach($t as $h) {
$this->options['host'][]='ldaps://'.$h;
}
if(!isset($options['port']) || empty($options['port'])) {
$this->options['port'] = 636;
}
} else {
$this->options['host']=$t;
}
}
if(isset($options['port']) && !empty($options['port'])) {
$this->options['port']=intval($options['port']);
}
if(isset($options['version']) && !empty($options['version'])) {
$this->options['version']=intval($options['version']);
}
if(isset($options['startls']) && !empty($options['startls'])) {
$this->options['startls']=($options['startls']===true || $options['startls']=='y');
}
if(isset($options['groupmemberisdn']) && !empty($options['groupmemberisdn'])) {
$this->options['groupmemberisdn']=($options['groupmemberisdn']===true || $options['groupmemberisdn']=='y');
}
// only string checking fo these ones
foreach(array('basedn','username','password','userdn','useroc','userattr',
'fullnameattr','emailattr','groupdn','groupattr','groupoc','groupnameattr',
'groupdescattr','groupmemberattr','usergroupattr','groupgroupattr') as $n) {
if(isset($options[$n]) && !empty($options[$n]) && preg_match('#\s#',$options[$n])==0) {
$this->options[$n] = $options[$n];
}
}
if(empty($this->options['groupgroupattr'])) $this->options['groupgroupattr']=$this->options['usergroupattr'];
if(isset($options['password'])) $this->options['bindpw'] = $options['password'];
if(isset($options['scope']) && !empty($options['scope'])) {
switch($options['scope']) {
case 'sub':
case 'one':
case 'base':
$this->options['scope'] = $options['scope'];
break;
default:
break;
}
}
if(isset($options['bind_type']) && !empty($options['bind_type'])) {
switch($options['bind_type']) {
case 'ad':
case 'ol':
case 'full':
case 'plain':
$this->options['bind_type'] = $options['bind_type'];
break;
default:
break;
}
}
}
// End public function TikiLdapLib($options)
private function __destruct() {
unset($this->ldaplink);
}
// Do a ldap bind
public function bind() {
if($this->ldaplink instanceof Net_LDAP2) {
return (true); // do not try to reconnect since this may lead to huge timeouts
}
$user=$this->options['username'];
switch ($this->options['bind_type']) {
case 'ad': // active directory
preg_match_all('/\s*,?dc=\s*([^,]+)/i',$this->options['basedn'],$t);
$this->options['binddn'] = $user.'@';
// print_r($t[1]);
if(isset($t[1]) && is_array($t[1])) {
foreach($t[1] as $domainpart) {
$this->options['binddn'] .= $domainpart.'.';
}
// cut trailing dot
$this->options['binddn']=substr($this->options['binddn'],0,-1);
}
// set referrals to 0 to avoid LDAP_OPERATIONS_ERROR
$this->options['options']['LDAP_OPT_REFERRALS']=0;
break;
case 'plain': // plain username
$this->options['binddn'] = $user;
break;
case 'full':
$this->options['binddn'] = $this->user_dn($user);
break;
case 'ol': // openldap
default:
$this->options['binddn'] = 'cn='.$user.','.$prefs['auth_ldap_basedn'];
break;
}
// attributes to fetch
/*
$options['attributes'] = array();
if ( $nameattr = $prefs['auth_ldap_nameattr'] ) $options['attributes'][] = $nameattr;
if ( $countryattr = $prefs['auth_ldap_countryattr'] ) $options['attributes'][] = $countryattr;
if ( $emailattr = $prefs['auth_ldap_emailattr'] ) $options['attributes'][] = $emailattr;
*/
// print("<pre>");print_r($options);print("</pre>");
$this->add_log('ldap','Connect Host: '.implode($this->options['host']).'. Binddn: '.
$this->options['binddn'].' at line '.__LINE__.' in '.__FILE__);
//create options array to handle it to Net_LDAP2
foreach(array('host','port','version','starttls','basedn','filter','scope','binddn','bindpw','options')
as $o) {
if(isset($this->options[$o])) {
$options[$o] = $this->options[$o];
}
}
$this->ldaplink= Net_LDAP2::connect($options);
if(Net_LDAP2::isError($this->ldaplink)) {
$this->add_log('ldap','Error: '.$this->ldaplink->getMessage().' at line '.__LINE__.' in '.__FILE__);
// return Net_LDAP2 Error codes. No need to redefine this.
return($this->ldaplink->getCode());
}
return LDAP_SUCCESS;
} // End bind()
// return information about user attributes
public function get_user_attributes() {
if(!empty($this->user_attributes)) { //been there, done that
return($this->user_attributes);
}
$userdn=$this->user_dn();
// ensure we have a connection to the ldap server
if(!$this->bind()) {
$this->add_log('ldap','Reuse of ldap connection failed: '.$this->ldaplink->getMessage().' at line '.__LINE__.' in '.__FILE__);
return false;
}
// todo: only fetch needed attributes
$entry = $this->ldaplink->getEntry($userdn);
if(Net_LDAP2::isError($entry)) { // wrong userdn. So we have to search
// prepare Search Filter
$filter=Net_LDAP2_Filter::create($this->options['userattr'],'equals',$this->options['username']);
$searchoptions=array('scope' => $this->options['scope']);
$searchresult = $this->ldaplink->search($this->userbase_dn(),$filter,$searchoptions);
if(Net_LDAP2::isError($searchresult)) {
$this->add_log('ldap','Search failed: '.$searchresult->getMessage().' at line '.__LINE__.' in '.__FILE__);
return false;
}
if($searchresult->count()!=1) {
$this->add_log('ldap','Error: Search returned '.$searchresult->count().' entries'.' at line '.__LINE__.' in '.__FILE__);
return false;
}
// get first entry
$entry=$searchresult->shiftEntry();
}
$this->user_attributes = $entry->getValues();
$this->user_attributes['dn'] = $entry->dn();
if (Net_LDAP2::isError($this->user_attributes)) {
$this->add_log('ldap','Error fetching user attributes: '.$this->user_attributes->getMessage().' at line '.__LINE__.' in '.__FILE__);
return false;
}
return($this->user_attributes);
} // End: public function get_user_attributes()
// return dn of all groups a user belongs to
public function get_groups() {
$filter1=Net_LDAP2_Filter::create('objectClass','equals',$this->options['groupoc']);
if(!empty($this->options['groupmemberattr'])) {
// get membership from group information
if($this->options['groupmemberisdn']) {
$filter2=Net_LDAP2_Filter::create($this->options['groupmemberattr'],'equals',$this->user_dn());
} else {
$filter2=Net_LDAP2_Filter::create($this->options['groupmemberattr'],'equals',$this->options['username']);
}
$filter=Net_LDAP2_Filter::combine('and',array($filter1,$filter2));
} else if(!empty($this->options['usergroupattr'])) {
// get membership from user information
$ugi=&$this->user_attributes[$this->options['usergroupattr']];
if(!is_array($ugi)) {
$ugi=array($ugi);
}
foreach($ugi as $g) {
$filter2=Net_LDAP2_Filter::create($this->options['groupgroupattr'],'equals',$g);
$filter3=Net_LDAP2_Filter::combine('or',array($filter1,$filter2));
}
$filter=Net_LDAP2_Filter::combine('and',array($filter1,$filter3));
} else {
// not possible to get groups - return empty array
return(array());
}
if(Net_LDAP2::isError($filter)) {
$this->add_log('ldap','LDAP Filter creation error: '.$filter->getMessage().' at line '.__LINE__.' in '.__FILE__);
return false;
}
$searchoptions=array('scope' => $this->options['scope']);
$searchresult = $this->ldaplink->search($this->groupbase_dn(),$filter,$searchoptions);
if(Net_LDAP2::isError($searchresult)) {
$this->add_log('ldap','Search failed: '.$searchresult->getMessage().' at line '.__LINE__.' in '.__FILE__);
return false;
}
while($entry=$searchresult->shiftEntry()) {
if (Net_LDAP2::isError($entry)) {
$this->add_log('ldap','Error fetching group entries: '.$entry->getMessage().' at line '.__LINE__.' in '.__FILE__);
return false;
}
$this->groups[$entry->dn()]=$entry->getValues(); // no error checking necessary here
}
return($this->groups);
} // End: private function get_group_dns()
// helper funtions
private function userbase_dn() {
if(empty($this->options['userdn']))
return($this->options['basedn']);
return($this->options['userdn'].','.$this->options['basedn']);
}
private function user_dn() {
if(isset($this->user_attributes['dn'])) {
// we did already fetch user attributes and have the real dn now
return($this->user_attributes['dn']);
}
if(empty($this->options['userattr'])) {
$ua='cn=';
} else {
$ua=$this->options['userattr'].'=';
}
return($ua.$this->options['username'].','.$this->userbase_dn());
}
private function groupbase_dn() {
if(empty($this->options['groupdn']))
return($this->options['basedn']);
return($this->options['groupdn'].','.$this->options['basedn']);
}
private function add_log($facility,$message) {
if($this->options['debug'])
$this->logslib->add_log($facility,$message);
}
}
?>
<?php
/* vim: set expandtab tabstop=4 shiftwidth=4: */
/**
* File containing the Net_LDAP2 interface class.
*
* PHP version 5
*
* @category Net
* @package Net_LDAP2
* @author Tarjej Huse <tarjei@bergfald.no>
* @author Jan Wagner <wagner@netsols.de>
* @author Del <del@babel.com.au>
* @author Benedikt Hallinger <beni@php.net>
* @copyright 2003-2007 Tarjej Huse, Jan Wagner, Del Elson, Benedikt Hallinger
* @license http://www.gnu.org/copyleft/lesser.html LGPL
* @version CVS: $Id: LDAP2.php,v 1.22 2009/06/02 12:57:09 beni Exp $
* @link http://pear.php.net/package/Net_LDAP2/
*/
/**
* Package includes.
*/
require_once 'PEAR.php';
require_once 'Net/LDAP2/RootDSE.php';
require_once 'Net/LDAP2/Schema.php';
require_once 'Net/LDAP2/Entry.php';
require_once 'Net/LDAP2/Search.php';
require_once 'Net/LDAP2/Util.php';
require_once 'Net/LDAP2/Filter.php';
require_once 'Net/LDAP2/LDIF.php';
require_once 'Net/LDAP2/SchemaCache.interface.php';
require_once 'Net/LDAP2/SimpleFileSchemaCache.php';
/**
* Error constants for errors that are not LDAP errors.
*/
define('NET_LDAP2_ERROR', 1000);
/**
* Net_LDAP2 Version
*/
define('NET_LDAP2_VERSION', '2.0.0');
/**
* Net_LDAP2 - manipulate LDAP servers the right way!
*
* @category Net
* @package Net_LDAP2
* @author Tarjej Huse <tarjei@bergfald.no>
* @author Jan Wagner <wagner@netsols.de>
* @author Del <del@babel.com.au>
* @author Benedikt Hallinger <beni@php.net>
* @copyright 2003-2007 Tarjej Huse, Jan Wagner, Del Elson, Benedikt Hallinger
* @license http://www.gnu.org/copyleft/lesser.html LGPL
* @link http://pear.php.net/package/Net_LDAP2/
*/
class Net_LDAP2 extends PEAR
{
/**
* Class configuration array
*
* host = the ldap host to connect to
* (may be an array of several hosts to try)
* port = the server port
* version = ldap version (defaults to v 3)
* starttls = when set, ldap_start_tls() is run after connecting.
* bindpw = no explanation needed
* binddn = the DN to bind as.
* basedn = ldap base
* options = hash of ldap options to set (opt => val)
* filter = default search filter
* scope = default search scope
*
* Newly added in 2.0.0RC4, for auto-reconnect:
* auto_reconnect = if set to true then the class will automatically
* attempt to reconnect to the LDAP server in certain
* failure conditionswhen attempting a search, or other
* LDAP operation. Defaults to false. Note that if you
* set this to true, calls to search() may block
* indefinitely if there is a catastrophic server failure.
* min_backoff = minimum reconnection delay period (in seconds).
* current_backoff = initial reconnection delay period (in seconds).
* max_backoff = maximum reconnection delay period (in seconds).
*
* @access protected
* @var array
*/
protected $_config = array('host' => 'localhost',
'port' => 389,
'version' => 3,
'starttls' => false,
'binddn' => '',
'bindpw' => '',
'basedn' => '',
'options' => array(),
'filter' => '(objectClass=*)',
'scope' => 'sub',
'auto_reconnect' => false,
'min_backoff' => 1,
'current_backoff' => 1,
'max_backoff' => 32);
/**
* List of hosts we try to establish a connection to
*
* @access protected
* @var array
*/
protected $_host_list = array();
/**
* List of hosts that are known to be down.
*
* @access protected
* @var array
*/
protected $_down_host_list = array();
/**
* LDAP resource link.
*
* @access protected
* @var resource
*/
protected $_link = false;
/**
* Net_LDAP2_Schema object
*
* This gets set and returned by {@link schema()}
*
* @access protected
* @var object Net_LDAP2_Schema
*/
protected $_schema = null;
/**
* Schema cacher function callback
*
* @see registerSchemaCache()
* @var string
*/
protected $_schema_cache = null;
/**
* Cache for attribute encoding checks
*
* @access protected
* @var array Hash with attribute names as key and boolean value
* to determine whether they should be utf8 encoded or not.
*/
protected $_schemaAttrs = array();
/**
* Cache for rootDSE objects
*
* Hash with requested rootDSE attr names as key and rootDSE object as value
*
* Since the RootDSE object itself may request a rootDSE object,
* {@link rootDse()} caches successful requests.
* Internally, Net_LDAP2 needs several lookups to this object, so
* caching increases performance significally.
*
* @access protected
* @var array
*/
protected $_rootDSE_cache = array();
/**
* Returns the Net_LDAP2 Release version, may be called statically
*
* @static
* @return string Net_LDAP2 version
*/
public static function getVersion()
{
return NET_LDAP2_VERSION;
}
/**
* Configure Net_LDAP2, connect and bind
*
* Use this method as starting point of using Net_LDAP2
* to establish a connection to your LDAP server.
*
* Static function that returns either an error object or the new Net_LDAP2
* object. Something like a factory. Takes a config array with the needed
* parameters.
*
* @param array $config Configuration array
*
* @access public
* @return Net_LDAP2_Error|Net_LDAP2 Net_LDAP2_Error or Net_LDAP2 object
*/
public static function &connect($config = array())
{
$ldap_check = self::checkLDAPExtension();
if (self::iserror($ldap_check)) {
return $ldap_check;
}
@$obj = new Net_LDAP2($config);
// todo? better errorhandling for setConfig()?
// connect and bind with credentials in config
$err = $obj->bind();
if (self::isError($err)) {
return $err;
}
return $obj;
}
/**
* Net_LDAP2 constructor
*
* Sets the config array
*
* Please note that the usual way of getting Net_LDAP2 to work is
* to call something like:
* <code>$ldap = Net_LDAP2::connect($ldap_config);</code>
*
* @param array $config Configuration array
*
* @access protected
* @return void
* @see $_config
*/
public function __construct($config = array())
{
$this->PEAR('Net_LDAP2_Error');
$this->setConfig($config);
}
/**
* Sets the internal configuration array
*
* @param array $config Configuration array
*
* @access protected
* @return void
*/
protected function setConfig($config)
{
//
// Parameter check -- probably should raise an error here if config
// is not an array.
//
if (! is_array($config)) {
return;
}
foreach ($config as $k => $v) {
if (isset($this->_config[$k])) {
$this->_config[$k] = $v;
} else {
// map old (Net_LDAP2) parms to new ones
switch($k) {
case "dn":
$this->_config["binddn"] = $v;
break;
case "password":
$this->_config["bindpw"] = $v;
break;
case "tls":
$this->_config["starttls"] = $v;
break;
case "base":
$this->_config["basedn"] = $v;
break;
}
}
}
//
// Ensure the host list is an array.
//
if (is_array($this->_config['host'])) {
$this->_host_list = $this->_config['host'];
} else {
if (strlen($this->_config['host']) > 0) {
$this->_host_list = array($this->_config['host']);
} else {
$this->_host_list = array();
// ^ this will cause an error in performConnect(),
// so the user is notified about the failure
}
}
//
// Reset the down host list, which seems like a sensible thing to do
// if the config is being reset for some reason.
//
$this->_down_host_list = array();
}
/**
* Bind or rebind to the ldap-server
*
* This function binds with the given dn and password to the server. In case
* no connection has been made yet, it will be started and startTLS issued
* if appropiate.
*
* The internal bind configuration is not being updated, so if you call
* bind() without parameters, you can rebind with the credentials
* provided at first connecting to the server.
*
* @param string $dn Distinguished name for binding
* @param string $password Password for binding
*
* @access public
* @return Net_LDAP2_Error|true Net_LDAP2_Error object or true
*/
public function bind($dn = null, $password = null)
{
// fetch current bind credentials
if (is_null($dn)) {
$dn = $this->_config["binddn"];
}
if (is_null($password)) {
$password = $this->_config["bindpw"];
}
// Connect first, if we haven't so far.
// This will also bind us to the server.
if ($this->_link === false) {
// store old credentials so we can revert them later
// then overwrit