ldap.php 15.3 KB
Newer Older
1
<?php
2
// (c) Copyright 2002-2013 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
 *  Class that adds LDAP Authentication to Tiki and aids Tiki to get User/Group Information
 *  from a LDAP directory
 */


14 15
class TikiLdapLib
{
16

17
	// var to hold a established connection
18 19 20 21 22
	protected $ldaplink = NULL;

	// var for ldap configuration parameters
	protected $options = array(
		'host' => 'localhost',
23
		'port' => NULL,
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
		'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' => '',
		'groupmemberattr' => '',
		'groupmemberisdn' => true,
		'usergroupattr' => '',
		'groupgroupattr' => '',
		'debug' => false
	);

51
	protected $logslib = NULL;
52

53 54 55 56 57
	/**
	 * @var array The user attributes
	 */
	protected $user_attributes = null;

58
	// Constructor
59 60
	public function __construct($options)
	{
61
		// debug setting
lphuberdeau's avatar
lphuberdeau committed
62
		$logslib = TikiLib::lib('logs');
63 64
		if (isset($options['debug']) && ($options['debug']===true || $options['debug']=='y' )&& ($logslib instanceof LogsLib)) {
			$this->options['debug'] = true;
lphuberdeau's avatar
lphuberdeau committed
65
			$this->logslib = $logslib;
66 67
		}
		// Configure the connection
68

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

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

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

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

103 104
		if (isset($options['starttls']) && !empty($options['starttls'])) {
			$this->options['starttls'] = ($options['starttls'] === true || $options['starttls'] == 'y');
105 106
		}

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

		// only string checking fo these ones
112
		foreach (array('basedn', 'username', 'password', 'userdn', 'useroc', 'userattr',
113
				'fullnameattr', 'emailattr', 'groupdn', 'groupattr', 'groupoc', 'groupnameattr',
114 115
				'groupdescattr', 'groupmemberattr', 'usergroupattr', 'groupgroupattr', 'binddn', 'bindpw') as $n) {
			if (isset($options[$n]) && !empty($options[$n])) {
116 117 118 119
				$this->options[$n] = $options[$n];
			}
		}

120 121
		if (empty($this->options['groupgroupattr']))
			$this->options['groupgroupattr'] = $this->options['usergroupattr'];
122

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

126
		if (isset($options['scope']) && !empty($options['scope'])) {
127 128 129 130 131
			switch($options['scope']) {
				case 'sub':
				case 'one':
				case 'base':
					$this->options['scope'] = $options['scope'];
132
					break;
133

134
				default:
135
					break;
136 137 138
			}
		}

139
		if (isset($options['bind_type']) && !empty($options['bind_type'])) {
140 141 142 143 144
			switch($options['bind_type']) {
				case 'ad':
				case 'ol':
				case 'full':
				case 'plain':
145
				case 'explicit':
146
					$this->options['bind_type'] = $options['bind_type'];
147
					break;
148

149
				default:
150
					break;
151 152 153 154 155
			}
		}
	}
	// 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 bindpw with the options['password']
176 177 178
		if ($this->options['bind_type'] != 'explicit') {
			$this->options['bindpw'] = $this->options['password'];
		}
179

180
		$user = $this->options['username'];
181 182
		switch ($this->options['bind_type']) {
			case 'ad': // active directory
183
				preg_match_all('/\s*,?dc=\s*([^,]+)/i', $this->options['basedn'], $t);
184
				$this->options['binddn'] = $user.'@';
185

186
				if (isset($t[1]) && is_array($t[1])) {
187
					foreach ($t[1] as $domainpart) {
188 189 190
						$this->options['binddn'] .= $domainpart.'.';
					}
					// cut trailing dot
191
					$this->options['binddn'] = substr($this->options['binddn'], 0, -1);
192 193
				}
				// set referrals to 0 to avoid LDAP_OPERATIONS_ERROR
194
				$this->options['options']['LDAP_OPT_REFERRALS'] = 0;
195
				break;
196

197 198
			case 'plain': // plain username
				$this->options['binddn'] = $user;
199
				break;
200

201 202
			case 'full':
				$this->options['binddn'] = $this->user_dn($user);
203
				break;
204

205
			case 'ol': // openldap
206
				$this->options['binddn'] = 'cn=' . $user . ',' . $prefs['auth_ldap_basedn'];
207
				break;
208

209
			case 'default':
210
				// Anonymous binding
211 212
				$this->options['binddn'] = '';
				$this->options['bindpw'] = '';
213
				break;
214

215
			case 'explicit':
216
				break;
217

218
			default:
219
				$this->add_log('ldap', 'Error: Invalid "bind_type" value "' . $this->options['bind_type'] . '".');
220
				die;
221
		}
222

223
		$this->add_log(
224 225
			'ldap',
			'Connect Host: ' . implode($this->options['host']) . '. Binddn: '. $this->options['binddn'] . ' at line ' . __LINE__ . ' in ' . __FILE__
226
		);
227 228

		//create options array to handle it to Net_LDAP2
229
		foreach (array('host', 'port', 'version', 'starttls', 'basedn', 'filter', 'scope', 'binddn', 'bindpw', 'options') as $o) {
230
			if (isset($this->options[$o])) {
231 232 233
				$options[$o] = $this->options[$o];
			}
		}
234

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

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

245

246 247

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

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

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

		// todo: only fetch needed attributes
266

267
		//A non-existing user may not return ldaplink->getEntry (found bug on windows server), if not found, user input incorrect username/password
268
		if (method_exists($this->ldaplink, 'getEntry')) {
269 270 271 272
			$entry = $this->ldaplink->getEntry($userdn);
		} else {
			return false;
		}
273

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

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

		return($this->user_attributes);

	} // End: public function get_user_attributes()

303 304 305 306 307
	// Request all users attributes
	public function get_all_users_attributes()
	{
		// ensure we have a connection to the ldap server
		if ($this->bind() != 'LDAP_SUCCESS') {
308
			$this->add_log('ldap', 'Reuse of ldap connection failed: ' . $this->ldaplink->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
309 310 311 312
			return false;
		}

		// Prepare Search Filter
313 314 315
		$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__);
316

317
		$searchresult = $this->ldaplink->search($this->userbase_dn(), $filter, $searchoptions);
318 319

		if (Net_LDAP2::isError($searchresult)) {
320
			$this->add_log('ldap', 'Search failed: ' . $searchresult->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
321 322 323 324
			return false;
		}

		if ($searchresult->count() < 1) {
325
			$this->add_log('ldap', 'Error: Search returned ' . $searchresult->count() . ' entries' . ' at line ' . __LINE__ . ' in ' . __FILE__);
326 327 328 329 330 331 332 333 334 335 336
			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)) {
337
				$this->add_log('ldap', 'Error fetching user attributes: ' . $user_attributes->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
338 339 340 341 342
				return false;
			}

			$users_attributes[] = $user_attributes;
		}
343

344
		return ($users_attributes);
345

346
	} // End: public function get_user_attributes()
347 348

	// return dn of all groups a user belongs to
349
	public function get_groups($force_reload = false)
350
	{
351 352
		$this->get_user_attributes($force_reload);

353
		// ensure we have a connection to the ldap server
354
		if ($this->bind() != 'LDAP_SUCCESS') {
355 356 357
			$this->add_log('ldap', 'Reuse of ldap connection failed: ' . $this->ldaplink->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
			return false;
		}
358 359


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

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

374
		} else if (!empty($this->options['usergroupattr'])) {
375
			// get membership from user information
376 377 378 379 380 381

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

				if (!is_array($ugi)) {
					$ugi = array($ugi);
382
				}
383

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

394
				$filter = Net_LDAP2_Filter::combine('and', array($filter1, $filter3));
395
			} else { // User has no group
396
				$filter = NULL;
397
			}
398 399 400 401
		} else {
			// not possible to get groups - return empty array
			return(array());
		}
402 403 404

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

408
		$this->add_log(
409 410 411
			'ldap',
			'Searching for group entries with filter: ' . $filter->asString() . ' base ' .
			$this->groupbase_dn() . ' at line ' . __LINE__ . ' in ' . __FILE__
412 413
		);

414 415
		$searchoptions = array('scope' => $this->options['scope']);
		$searchresult = $this->ldaplink->search($this->groupbase_dn(), $filter, $searchoptions);
416

417
		if (Net_LDAP2::isError($searchresult)) {
418
			$this->add_log('ldap', 'Search failed: ' . $searchresult->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
419 420
			return false;
		}
421 422
		$this->add_log('ldap', 'Found ' . $searchresult->count() . ' entries. Extracting entries now.');

423
		$this->groups = array();
424
		while ($entry = $searchresult->shiftEntry()) {
425
			if (Net_LDAP2::isError($entry)) {
426
				$this->add_log('ldap', 'Error fetching group entries: ' . $entry->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
427 428
				return false;
			}
429
			$this->groups[$entry->dn()] = $entry->getValues(); // no error checking necessary here
430
		}
431
		$this->add_log('ldap', count($this->groups) . ' groups found at line ' . __LINE__ . ' in ' . __FILE__);
432 433 434 435 436 437 438 439

		return($this->groups);

	} // End: private function get_group_dns()




440 441 442 443
	// helper functions
	private function userbase_dn()
	{
		if (empty($this->options['userdn']))
444
			return($this->options['basedn']);
445
		return($this->options['userdn'] . ',' . $this->options['basedn']);
446 447
	}

448 449 450
	private function user_dn()
	{
		if (isset($this->user_attributes['dn'])) {
451 452 453
			// we did already fetch user attributes and have the real dn now
			return($this->user_attributes['dn']);
		}
454 455
		if (empty($this->options['userattr'])) {
			$ua = 'cn=';
456
		} else {
457 458 459
			$ua = $this->options['userattr'] . '=';
		}
		return($ua.$this->options['username'] . ',' . $this->userbase_dn());
460 461
	}

462 463 464
	private function groupbase_dn()
	{
		if (empty($this->options['groupdn']))
465
			return($this->options['basedn']);
466
		return($this->options['groupdn'] . ',' . $this->options['basedn']);
467 468
	}

469 470 471 472
	private function add_log($facility, $message)
	{
		if ($this->options['debug'])
			$this->logslib->add_log($facility, $message);
473 474
	}

475
	/**
476
	 * Setter to set an option value
477 478 479 480 481
	 * @param string $name The name of the option
	 * @param mixed $value The value
	 * @return void
	 * @throw Exception
	 */
482 483
	public function setOption ($name, $value = null)
	{
484 485 486 487 488
		if (isset($this->options[$name])) {
			$this->options[$name] = $value;
		} else {
			throw new Exception(sprintf("Undefined option: %s \n", $name), E_USER_WARNING);
		}
489 490 491
	}

	/**
marclaporte's avatar
marclaporte committed
492
	 * Return the value of the attribute past in param
493 494 495 496
	 * @param string $name The name of the attribute
	 * @return mixed
	 * @throw Exception
	 */
497 498
	public function getUserAttribute ($name)
	{
499 500 501 502 503 504 505 506
		$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);
			}
507 508 509
		} catch (Exception $e) {

		}
510
		return $value;
511
	}
512
}