ldap.php 13.6 KB
Newer Older
1
<?php
2 3 4 5 6
// (c) Copyright 2002-2010 by authors of the Tiki Wiki/CMS/Groupware Project
// 
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
// $Id$
7 8 9 10 11 12 13 14 15 16

/* 
 *  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");


17 18
class TikiLdapLib
{
19 20 21 22 23 24 25

	// var to hold a esablished connection
	protected $ldaplink = NULL;

	// var for ldap configuration parameters
	protected $options = array(
		'host' => 'localhost',
26
		'port' => NULL,
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
		'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
	);

56
	protected $logslib = NULL;
57

58 59 60 61 62
	/**
	 * @var array The user attributes
	 */
	protected $user_attributes = null;

63
	// Constructor
64 65
	public function __construct($options)
	{
66 67
		// debug setting
		global $logslib;
68 69 70
		if (isset($options['debug']) && ($options['debug']===true || $options['debug']=='y' )&& ($logslib instanceof LogsLib)) {
			$this->options['debug'] = true;
			$this->logslib = &$logslib;
71 72
		}
		// Configure the connection
73

74 75
		// host can be a list of hostnames.
		// It is easier to create URIs because if we use ssl, we have to create a URI
76 77
		if (isset($options['host']) && !empty($options['host'])) {
			$h = $options['host'];
78
		} else { // use default
79
			$h = $this->options['host'];
80
		}
81

82
		$t=preg_split('#[\s,]#',$h);
83 84 85
		if (isset($options['ssl']) && ($options['ssl']=='y' || $options['ssl']===true)) {
			$prefix = 'ldaps://';
			$port = 636;
86
		} else {
87 88
			$prefix = 'ldap://';
			$port = 389;
89
		}
90
		if (isset($options['port']) && !empty($options['port'])) {
91 92
			$port=intval($options['port']);
		}
93
		$this->options['port'] = NULL; // its save to set port in URI
94

95
		$this->options['host'] = array();
96
		foreach($t as $h) {
97 98
			if (preg_match('#^ldaps?://#',$h)) { // entry is already URI
				$this->options['host'][] = $h;
99
			} else {
100
				$this->options['host'][] = $prefix . $h . ':' . $port;
101
			}
102
		}
103 104 105

		if (isset($options['version']) && !empty($options['version'])) {
			$this->options['version'] = intval($options['version']);
106 107
		}

108 109
		if (isset($options['startls']) && !empty($options['startls'])) {
			$this->options['startls'] = ($options['startls']===true || $options['startls']=='y');
110 111
		}

112 113
		if (isset($options['groupmemberisdn']) && !empty($options['groupmemberisdn'])) {
			$this->options['groupmemberisdn'] = ($options['groupmemberisdn']===true || $options['groupmemberisdn']=='y');
114 115 116
		}

		// only string checking fo these ones
117 118 119 120
		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) {
121 122 123 124
				$this->options[$n] = $options[$n];
			}
		}

125 126 127
		if (empty($this->options['groupgroupattr'])) $this->options['groupgroupattr']=$this->options['usergroupattr'];

		if (isset($options['password'])) $this->options['bindpw'] = $options['password'];
128

129
		if (isset($options['scope']) && !empty($options['scope'])) {
130 131 132 133 134 135 136 137 138 139 140
			switch($options['scope']) {
				case 'sub':
				case 'one':
				case 'base':
					$this->options['scope'] = $options['scope'];
					break;
				default:
					break;
			}
		}

141
		if (isset($options['bind_type']) && !empty($options['bind_type'])) {
142 143 144 145 146 147 148 149 150 151 152 153 154 155
			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)

156 157
	public function __destruct()
	{
158 159 160 161
		unset($this->ldaplink);
	}

	// Do a ldap bind
162 163
	public function bind( $reconnect = false )
	{
164
		global $prefs;
165 166

		// Force the reconnection
167 168 169 170 171 172
		if ($this->ldaplink instanceof Net_LDAP2) {
				if ($reconnect === true) {
						$this->ldaplink->disconnect();
				} else {
						return (true); // do not try to reconnect since this may lead to huge timeouts
				}
173
		}
174 175

		// Set the bindnpw with the options['password']
176 177
		$this->options['bindpw'] = $this->options['password'];

178
		$user = $this->options['username'];
179 180
		switch ($this->options['bind_type']) {
			case 'ad': // active directory
181
				preg_match_all('/\s*,?dc=\s*([^,]+)/i',$this->options['basedn'], $t);
182
				$this->options['binddn'] = $user.'@';
183
				if (isset($t[1]) && is_array($t[1])) {
184 185 186 187
					foreach($t[1] as $domainpart) {
						$this->options['binddn'] .= $domainpart.'.';
					}
					// cut trailing dot
188
					$this->options['binddn']=substr($this->options['binddn'], 0, -1);
189 190 191
				}
				// set referrals to 0 to avoid LDAP_OPERATIONS_ERROR
				$this->options['options']['LDAP_OPT_REFERRALS']=0;
chealer's avatar
chealer committed
192
				break;	
193 194 195 196 197 198 199
			case 'plain': // plain username
				$this->options['binddn'] = $user;
				break;
			case 'full':
				$this->options['binddn'] = $this->user_dn($user);
				break;
			case 'ol': // openldap
sylvieg's avatar
sylvieg committed
200
				$this->options['binddn'] = 'cn='.$user.','.$prefs['auth_ldap_basedn'];
201
				break;
202
			case 'default':
203
				// Anonymous binding
204 205 206
				$this->options['binddn'] = '';
				$this->options['bindpw'] = '';
				break;
207
			default:
208
				$this->add_log('ldap', 'Error: Invalid "bind_type" value "' . $this->options['bind_type'] . '".');
209
				die;
210
		}
211

212
		// attributes to fetch
213 214 215 216 217 218 219 220
/*
        $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;
*/


221
		$this->add_log('ldap', 'Connect Host: '.implode($this->options['host']).'. Binddn: '.
222 223
					$this->options['binddn'].' at line '.__LINE__.' in '.__FILE__);
		//create options array to handle it to Net_LDAP2
224
		foreach(array('host', 'port', 'version', 'starttls', 'basedn', 'filter', 'scope', 'binddn', 'bindpw', 'options')
225
				as $o) {
226
			if (isset($this->options[$o])) {
227 228 229
				$options[$o] = $this->options[$o];
			}
		}
230

231
		$this->ldaplink= Net_LDAP2::connect($options);
232 233
		if (Net_LDAP2::isError($this->ldaplink)) {
			$this->add_log('ldap', 'Error: ' . $this->ldaplink->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
234 235 236
			// return Net_LDAP2 Error codes. No need to redefine this.
			return($this->ldaplink->getCode());
		}
237

238
		return 'LDAP_SUCCESS';
239 240
	} // End bind()

241

242 243

	// return information about user attributes
244 245 246 247
	public function get_user_attributes()
	{

		if (!empty($this->user_attributes)) { //been there, done that
248 249 250
			return($this->user_attributes);
		}

251
		$userdn = $this->user_dn();
252
		// ensure we have a connection to the ldap server
253 254
		 if (!$this->bind()) {
			$this->add_log('ldap','Reuse of ldap connection failed: ' . $this->ldaplink->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
255 256 257 258 259 260
			return false;
		}

		// todo: only fetch needed attributes

		$entry = $this->ldaplink->getEntry($userdn);
261
		if (Net_LDAP2::isError($entry)) { // wrong userdn. So we have to search
262
			// prepare Search Filter
263
			$filter = Net_LDAP2_Filter::create($this->options['userattr'], 'equals', $this->options['username']);
264
			$searchoptions=array('scope' => $this->options['scope']);
265 266 267 268
			$this->add_log('ldap', 'Searching for user information with filter: '.$filter->asString().' at line '.__LINE__.' in '.__FILE__);
			$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__);
269 270
				return false;
			}
271 272
			if ($searchresult->count() != 1) {
				$this->add_log('ldap', 'Error: Search returned ' . $searchresult->count() . ' entries' . ' at line ' . __LINE__ . ' in ' . __FILE__);
273 274 275
				return false;
			}
			// get first entry
276
			$entry = $searchresult->shiftEntry();
277 278 279 280 281

		}
		$this->user_attributes = $entry->getValues();
		$this->user_attributes['dn'] = $entry->dn();
		if (Net_LDAP2::isError($this->user_attributes)) {
282
			$this->add_log('ldap', 'Error fetching user attributes: ' . $this->user_attributes->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
283 284 285 286 287 288 289 290 291 292 293
			return false;
		}

		return($this->user_attributes);

	} // End: public function get_user_attributes()




	// return dn of all groups a user belongs to
294 295 296 297 298 299 300 301 302
	public function get_groups()
	{
		if (empty($this->user_attributes))
			$this->get_user_attributes();
		// 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;
		}
303 304


305
		$filter1 = Net_LDAP2_Filter::create('objectClass', 'equals', $this->options['groupoc']);
306

307
		if (!empty($this->options['groupmemberattr'])) {
308
			// get membership from group information
309 310
			if ($this->options['groupmemberisdn']) {
				$filter2 = Net_LDAP2_Filter::create($this->options['groupmemberattr'], 'equals', $this->user_dn());
311
			} else {
312
				$filter2 = Net_LDAP2_Filter::create($this->options['groupmemberattr'], 'equals', $this->options['username']);
313
			}
314
			$filter = Net_LDAP2_Filter::combine('and', array($filter1, $filter2));
315

316
		} else if (!empty($this->options['usergroupattr'])) {
317
			// get membership from user information
318 319 320 321 322 323

			$ugi = &$this->user_attributes[$this->options['usergroupattr']];
			if (!empty($ugi)) {

				if (!is_array($ugi)) {
					$ugi = array($ugi);
324
				}
325

326 327
				if (count($ugi) == 1) { // one gid
					$filter3 = Net_LDAP2_Filter::create($this->options['groupgroupattr'], 'equals', $ugi[0]);
328
				} else { // mor gids
329 330 331
					$filtertmp = array();
					foreach ($ugi as $g) {
						$filtertmp[] = Net_LDAP2_Filter::create($this->options['groupgroupattr'], 'equals', $g);
332
					}
333
					$filter3 = Net_LDAP2_Filter::combine('or', $filtertmp);
334
				}
335

336
				$filter = Net_LDAP2_Filter::combine('and', array($filter1, $filter3));
337
			} else { // User has no group
338
				$filter = NULL;
339
			}
340 341 342 343
		} else {
			// not possible to get groups - return empty array
			return(array());
		}
344 345 346

		if (Net_LDAP2::isError($filter)) {
			$this->add_log('ldap', 'LDAP Filter creation error: ' . $filter->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
347 348 349
			return false;
		}

chealer's avatar
chealer committed
350
		$this->add_log('ldap', 'Searching for group entries with filter: ' . $filter->asString() . ' base ' . $this->groupbase_dn() . ' at line ' . __LINE__ . ' in ' . __FILE__);
351 352
		$searchoptions = array('scope' => $this->options['scope']);
		$searchresult = $this->ldaplink->search($this->groupbase_dn(), $filter, $searchoptions);
353

354 355
		if (Net_LDAP2::isError($searchresult)) {
			$this->add_log('ldap' , 'Search failed: ' . $searchresult->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
356 357
			return false;
		}
358 359
		$this->add_log('ldap', 'Found ' . $searchresult->count() . ' entries. Extracting entries now.');

360
		$this->groups = array();
361
		while ($entry = $searchresult->shiftEntry()) {
362
			if (Net_LDAP2::isError($entry)) {
363
				$this->add_log('ldap', 'Error fetching group entries: ' . $entry->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
364 365
				return false;
			}
366
			$this->groups[$entry->dn()] = $entry->getValues(); // no error checking necessary here
367
		}
368
		$this->add_log('ldap', count($this->groups) . ' groups found at line ' . __LINE__ . ' in ' . __FILE__);
369 370 371 372 373 374 375 376

		return($this->groups);

	} // End: private function get_group_dns()




377 378 379 380
	// helper functions
	private function userbase_dn()
	{
		if (empty($this->options['userdn']))
381
			return($this->options['basedn']);
382
		return($this->options['userdn'] . ',' . $this->options['basedn']);
383 384
	}

385 386 387
	private function user_dn()
	{
		if (isset($this->user_attributes['dn'])) {
388 389 390
			// we did already fetch user attributes and have the real dn now
			return($this->user_attributes['dn']);
		}
391 392
		if (empty($this->options['userattr'])) {
			$ua = 'cn=';
393
		} else {
394 395 396
			$ua = $this->options['userattr'] . '=';
		}
		return($ua.$this->options['username'] . ',' . $this->userbase_dn());
397 398
	}

399 400 401
	private function groupbase_dn()
	{
		if (empty($this->options['groupdn']))
402
			return($this->options['basedn']);
403
		return($this->options['groupdn'] . ',' . $this->options['basedn']);
404 405
	}

406 407 408 409
	private function add_log($facility, $message)
	{
		if ($this->options['debug'])
			$this->logslib->add_log($facility, $message);
410 411
	}

412 413 414 415 416 417 418
	/**
	 * Setter to set an otpion value
	 * @param string $name The name of the option
	 * @param mixed $value The value
	 * @return void
	 * @throw Exception
	 */
419 420
	public function setOption ($name, $value = null)
	{
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435
		try {
			if (isset($this->options[$name])) {
				$this->options[$name] = $value;
			} else {
				throw new Exception(sprintf("Undefined option: %s \n", $name), E_USER_WARNING);
			}
		} catch (Exception $e) { }
	}

	/**
	 * Return the value of the attribue past in param
	 * @param string $name The name of the attribute
	 * @return mixed
	 * @throw Exception
	 */
436 437
	public function getUserAttribute ($name)
	{
438 439 440 441 442 443 444 445
		$value = '';
		try {
			$values = self::get_user_attributes();
			if (isset($values[$name])) {
				$value = $values[$name];
			} else {
				throw new Exception(sprintf("Undefined attribute %s \n", $name), E_USER_WARNING);
			}
446 447
		} catch (Exception $e) {}
		return $value;
448
	}
449
}