ldap.php 15.4 KB
Newer Older
1
<?php
2
// (c) Copyright 2002-2011 by authors of the Tiki Wiki CMS Groupware Project
3 4 5 6
// 
// 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
		foreach(array('basedn', 'username', 'password', 'userdn', 'useroc', 'userattr',
				'fullnameattr', 'emailattr', 'groupdn', 'groupattr', 'groupoc', 'groupnameattr',
119 120
				'groupdescattr', 'groupmemberattr', 'usergroupattr', 'groupgroupattr', 'binddn', 'bindpw') as $n) {
			if (isset($options[$n]) && !empty($options[$n])) {
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
			switch($options['bind_type']) {
				case 'ad':
				case 'ol':
				case 'full':
				case 'plain':
147
				case 'explicit':
148 149 150 151 152 153 154 155 156
					$this->options['bind_type'] = $options['bind_type'];
					break;
				default:
					break;
			}
		}
	}
	// End public function TikiLdapLib($options)

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

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

		// Force the reconnection
168 169 170 171 172 173
		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
				}
174
		}
175 176

		// Set the bindnpw with the options['password']
177 178 179
		if ($this->options['bind_type'] != 'explicit') {
			$this->options['bindpw'] = $this->options['password'];
		}
180

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

217
		// attributes to fetch
218 219 220 221 222 223 224 225
/*
        $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;
*/


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

236
		$this->ldaplink= Net_LDAP2::connect($options);
237 238
		if (Net_LDAP2::isError($this->ldaplink)) {
			$this->add_log('ldap', 'Error: ' . $this->ldaplink->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
239 240 241
			// return Net_LDAP2 Error codes. No need to redefine this.
			return($this->ldaplink->getCode());
		}
242

243
		return 'LDAP_SUCCESS';
244 245
	} // End bind()

246

247 248

	// return information about user attributes
249
	public function get_user_attributes($force_reload = false)
250
	{
251 252 253
		if ($force_reload) {
			unset($this->user_attributes);
		}
254

255 256
		if (!empty($this->user_attributes)) {
			return $this->user_attributes;
257 258
		}

259
		$userdn = $this->user_dn();
260
		// ensure we have a connection to the ldap server
261
		if ($this->bind() != 'LDAP_SUCCESS') {
262
			$this->add_log('ldap','Reuse of ldap connection failed: ' . $this->ldaplink->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
263 264 265 266 267 268
			return false;
		}

		// todo: only fetch needed attributes

		$entry = $this->ldaplink->getEntry($userdn);
269
		if ($force_reload || Net_LDAP2::isError($entry)) { // wrong userdn. So we have to search
270
			// prepare Search Filter
271
			$filter = Net_LDAP2_Filter::create($this->options['userattr'], 'equals', $this->options['username']);
272
			$searchoptions=array('scope' => $this->options['scope']);
273 274 275 276
			$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__);
277 278
				return false;
			}
279 280
			if ($searchresult->count() != 1) {
				$this->add_log('ldap', 'Error: Search returned ' . $searchresult->count() . ' entries' . ' at line ' . __LINE__ . ' in ' . __FILE__);
281 282 283
				return false;
			}
			// get first entry
284
			$entry = $searchresult->shiftEntry();
285 286 287 288 289

		}
		$this->user_attributes = $entry->getValues();
		$this->user_attributes['dn'] = $entry->dn();
		if (Net_LDAP2::isError($this->user_attributes)) {
290
			$this->add_log('ldap', 'Error fetching user attributes: ' . $this->user_attributes->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
291 292 293 294 295 296 297
			return false;
		}

		return($this->user_attributes);

	} // End: public function get_user_attributes()

298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337
	// Request all users attributes
	public function get_all_users_attributes()
	{
		// ensure we have a connection to the ldap server
		if ($this->bind() != 'LDAP_SUCCESS') {
			$this->add_log('ldap','Reuse of ldap connection failed: '.$this->ldaplink->getMessage().' at line '.__LINE__.' in '.__FILE__);
			return false;
		}

		// Prepare Search Filter
		$filter=Net_LDAP2_Filter::create('objectclass','equals',$this->options['useroc']);
		$searchoptions=array('scope' => $this->options['scope']);
		$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__);
			return false;
		}

		if ($searchresult->count() < 1) {
			$this->add_log('ldap','Error: Search returned '. $searchresult->count() .' entries'.' at line '.__LINE__.' in '.__FILE__);
			return false;
		}

		$entries = $searchresult->entries();
		$users_attributes = array();

		foreach ($entries as $entry) {
			$user_attributes = $entry->getValues();
			$user_attributes['dn'] = $entry->dn();

			if (Net_LDAP2::isError($user_attributes)) {
				$this->add_log('ldap','Error fetching user attributes: '. $user_attributes->getMessage().' at line '.__LINE__.' in '.__FILE__);
				return false;
			}

			$users_attributes[] = $user_attributes;
		}
338

339
		return ($users_attributes);
340

341
	} // End: public function get_user_attributes()
342 343

	// return dn of all groups a user belongs to
344
	public function get_groups($force_reload = false)
345
	{
346 347
		$this->get_user_attributes($force_reload);

348 349 350 351 352
		// 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;
		}
353 354


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

357
		if (!empty($this->options['groupmemberattr'])) {
358
			// get membership from group information
359
			if ($this->options['groupmemberisdn']) {
360 361 362
				if ($this->user_attributes['dn'] == null) {
					return false;
				}
363
				$filter2 = Net_LDAP2_Filter::create($this->options['groupmemberattr'], 'equals', $this->user_dn());
364
			} else {
365
				$filter2 = Net_LDAP2_Filter::create($this->options['groupmemberattr'], 'equals', $this->options['username']);
366
			}
367
			$filter = Net_LDAP2_Filter::combine('and', array($filter1, $filter2));
368

369
		} else if (!empty($this->options['usergroupattr'])) {
370
			// get membership from user information
371 372 373 374 375 376

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

				if (!is_array($ugi)) {
					$ugi = array($ugi);
377
				}
378

379 380
				if (count($ugi) == 1) { // one gid
					$filter3 = Net_LDAP2_Filter::create($this->options['groupgroupattr'], 'equals', $ugi[0]);
381
				} else { // mor gids
382 383 384
					$filtertmp = array();
					foreach ($ugi as $g) {
						$filtertmp[] = Net_LDAP2_Filter::create($this->options['groupgroupattr'], 'equals', $g);
385
					}
386
					$filter3 = Net_LDAP2_Filter::combine('or', $filtertmp);
387
				}
388

389
				$filter = Net_LDAP2_Filter::combine('and', array($filter1, $filter3));
390
			} else { // User has no group
391
				$filter = NULL;
392
			}
393 394 395 396
		} else {
			// not possible to get groups - return empty array
			return(array());
		}
397 398 399

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

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

407 408
		if (Net_LDAP2::isError($searchresult)) {
			$this->add_log('ldap' , 'Search failed: ' . $searchresult->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
409 410
			return false;
		}
411 412
		$this->add_log('ldap', 'Found ' . $searchresult->count() . ' entries. Extracting entries now.');

413
		$this->groups = array();
414
		while ($entry = $searchresult->shiftEntry()) {
415
			if (Net_LDAP2::isError($entry)) {
416
				$this->add_log('ldap', 'Error fetching group entries: ' . $entry->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
417 418
				return false;
			}
419
			$this->groups[$entry->dn()] = $entry->getValues(); // no error checking necessary here
420
		}
421
		$this->add_log('ldap', count($this->groups) . ' groups found at line ' . __LINE__ . ' in ' . __FILE__);
422 423 424 425 426 427 428 429

		return($this->groups);

	} // End: private function get_group_dns()




430 431 432 433
	// helper functions
	private function userbase_dn()
	{
		if (empty($this->options['userdn']))
434
			return($this->options['basedn']);
435
		return($this->options['userdn'] . ',' . $this->options['basedn']);
436 437
	}

438 439 440
	private function user_dn()
	{
		if (isset($this->user_attributes['dn'])) {
441 442 443
			// we did already fetch user attributes and have the real dn now
			return($this->user_attributes['dn']);
		}
444 445
		if (empty($this->options['userattr'])) {
			$ua = 'cn=';
446
		} else {
447 448 449
			$ua = $this->options['userattr'] . '=';
		}
		return($ua.$this->options['username'] . ',' . $this->userbase_dn());
450 451
	}

452 453 454
	private function groupbase_dn()
	{
		if (empty($this->options['groupdn']))
455
			return($this->options['basedn']);
456
		return($this->options['groupdn'] . ',' . $this->options['basedn']);
457 458
	}

459 460 461 462
	private function add_log($facility, $message)
	{
		if ($this->options['debug'])
			$this->logslib->add_log($facility, $message);
463 464
	}

465 466 467 468 469 470 471
	/**
	 * Setter to set an otpion value
	 * @param string $name The name of the option
	 * @param mixed $value The value
	 * @return void
	 * @throw Exception
	 */
472 473
	public function setOption ($name, $value = null)
	{
474 475 476 477 478 479 480 481 482 483 484 485 486 487 488
		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
	 */
489 490
	public function getUserAttribute ($name)
	{
491 492 493 494 495 496 497 498
		$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);
			}
499 500
		} catch (Exception $e) {}
		return $value;
501
	}
502
}