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

13 14 15
use Zend\Ldap\Filter;
use Zend\Ldap\Ldap;
use Zend\Ldap\Exception\LdapException;
16
use Zend\Ldap\Collection\DefaultIterator as LdapCollectionIterator;
17

18 19
class TikiLdapLib
{
20

21
	// var to hold a established connection
22
	/** @var Ldap */
rjsmelo's avatar
rjsmelo committed
23
	protected $ldaplink = null;
24 25

	// var for ldap configuration parameters
rjsmelo's avatar
rjsmelo committed
26
	protected $options = [
27
		'host' => 'localhost',
rjsmelo's avatar
rjsmelo committed
28
		'port' => null,
29 30
		'version' => 3,
		'starttls' => false,
31 32
		'useSsl' => false,
		'baseDn' => '',
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
		'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
rjsmelo's avatar
rjsmelo committed
54
	];
55

rjsmelo's avatar
rjsmelo committed
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
		// debug setting
lphuberdeau's avatar
lphuberdeau committed
67
		$logslib = TikiLib::lib('logs');
rjsmelo's avatar
rjsmelo committed
68
		if (isset($options['debug']) && ($options['debug'] === true || $options['debug'] == 'y' )&& ($logslib instanceof LogsLib)) {
69
			$this->options['debug'] = true;
lphuberdeau's avatar
lphuberdeau committed
70
			$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
rjsmelo's avatar
rjsmelo committed
76
		if (isset($options['host']) && ! empty($options['host'])) {
77
			$h = $options['host'];
78
		} else { // use default
79
			$h = $this->options['host'];
80
		}
81

82
		$t = preg_split('#[\s,]#', $h);
83
		if (isset($options['useSsl']) && ($options['useSsl'] == 'y' || $options['useSsl'] === true)) {
84 85
			$prefix = 'ldaps://';
			$port = 636;
86
		} else {
87 88
			$prefix = 'ldap://';
			$port = 389;
89
		}
rjsmelo's avatar
rjsmelo committed
90
		if (isset($options['port']) && ! empty($options['port'])) {
91
			$port = intval($options['port']);
92
		}
93 94

		$this->options['port'] = $port; // its save to set port in URI
95

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

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

rjsmelo's avatar
rjsmelo committed
109
		if (isset($options['groupmemberisdn']) && ! empty($options['groupmemberisdn'])) {
110
			$this->options['groupmemberisdn'] = ($options['groupmemberisdn'] === true || $options['groupmemberisdn'] == 'y');
111 112 113
		}

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

rjsmelo's avatar
rjsmelo committed
122
		if (empty($this->options['groupgroupattr'])) {
123
			$this->options['groupgroupattr'] = $this->options['usergroupattr'];
rjsmelo's avatar
rjsmelo committed
124
		}
125

rjsmelo's avatar
rjsmelo committed
126
		if (isset($options['password'])) {
127
			$this->options['bindpw'] = $options['password'];
rjsmelo's avatar
rjsmelo committed
128
		}
129

rjsmelo's avatar
rjsmelo committed
130 131
		if (isset($options['scope']) && ! empty($options['scope'])) {
			switch ($options['scope']) {
132 133 134 135
				case 'sub':
				case 'one':
				case 'base':
					$this->options['scope'] = $options['scope'];
136
					break;
137

138
				default:
139
					break;
140 141 142
			}
		}

rjsmelo's avatar
rjsmelo committed
143 144
		if (isset($options['bind_type']) && ! empty($options['bind_type'])) {
			switch ($options['bind_type']) {
145 146 147 148
				case 'ad':
				case 'ol':
				case 'full':
				case 'plain':
149
				case 'explicit':
150
					$this->options['bind_type'] = $options['bind_type'];
151
					break;
152

153
				default:
154
					break;
155 156 157 158 159
			}
		}
	}
	// End public function TikiLdapLib($options)

160 161
	public function __destruct()
	{
162 163 164 165
		unset($this->ldaplink);
	}

	// Do a ldap bind
rjsmelo's avatar
rjsmelo committed
166
	public function bind($reconnect = false)
167
	{
168
		global $prefs;
169 170

		// Force the reconnection
171
		if ($this->ldaplink instanceof Ldap && $this->ldaplink->getBoundUser() !== false) {
rjsmelo's avatar
rjsmelo committed
172 173 174
			if ($reconnect === true) {
					$this->ldaplink->disconnect();
			} else {
175
					return LdapException::LDAP_SUCCESS; // do not try to reconnect since this may lead to huge timeouts
rjsmelo's avatar
rjsmelo committed
176
			}
177
		}
178

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

184
		$user = $this->options['username'];
185 186
		switch ($this->options['bind_type']) {
			case 'ad': // active directory
187
				preg_match_all('/\s*,?dc=\s*([^,]+)/i', $this->options['baseDn'], $t);
rjsmelo's avatar
rjsmelo committed
188
				$this->options['binddn'] = $user . '@';
189

190
				if (isset($t[1]) && is_array($t[1])) {
191
					foreach ($t[1] as $domainpart) {
rjsmelo's avatar
rjsmelo committed
192
						$this->options['binddn'] .= $domainpart . '.';
193 194
					}
					// cut trailing dot
195
					$this->options['binddn'] = substr($this->options['binddn'], 0, -1);
196 197
				}
				// set referrals to 0 to avoid LDAP_OPERATIONS_ERROR
198
				$this->options['options']['LDAP_OPT_REFERRALS'] = 0;
199 200
				// use user@domain for binding
				$this->options['tryUsernameSplit'] = false;
201
				break;
202

203 204
			case 'plain': // plain username
				$this->options['binddn'] = $user;
205
				break;
206

207 208
			case 'full':
				$this->options['binddn'] = $this->user_dn($user);
209
				break;
210

211
			case 'ol': // openldap
212
				$this->options['binddn'] = 'cn=' . $user . ',' . $prefs['auth_ldap_basedn'];
213
				break;
214

215
			case 'default':
216
				// Anonymous binding
217 218
				$this->options['binddn'] = '';
				$this->options['bindpw'] = '';
219
				break;
220

221
			case 'explicit':
222
				break;
223

224
			default:
225
				$this->add_log('ldap', 'Error: Invalid "bind_type" value "' . $this->options['bind_type'] . '".');
226
				die;
227
		}
228

229
		$this->add_log(
230
			'ldap',
rjsmelo's avatar
rjsmelo committed
231
			'Connect Host: ' . implode($this->options['host']) . '. Binddn: ' . $this->options['binddn'] . ' at line ' . __LINE__ . ' in ' . __FILE__
232
		);
233

234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
		$permittedOptions = [
			'host',
			'port',
			'useSsl',
			'username',
			'password',
			'bindRequiresDn',
			'baseDn',
			'accountCanonicalForm',
			'accountDomainName',
			'accountDomainNameShort',
			'accountFilterFormat',
			'allowEmptyPassword',
			'useStartTls',
			'optReferrals',
			'tryUsernameSplit',
			'networkTimeout',
		];

		$options = [];
		//create options array to handle it to \Zend\Ldap\Ldap
		foreach ($permittedOptions as $o) {
256
			if (isset($this->options[$o])) {
257 258 259
				$options[$o] = $this->options[$o];
			}
		}
260

261 262 263 264 265 266 267 268
		try {
			$this->ldaplink = new Ldap($options);
			$this->ldaplink->bind($this->options['binddn'], $this->options['bindpw']);
		} catch (LdapException $e) {
			if ($prefs['auth_ldap_debug'] == 'y') {
				$this->add_log('ldap', 'Error: ' . $e->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
			}
			return $e->getCode();
269
		}
270

271
		return LdapException::LDAP_SUCCESS;
272 273
	} // End bind()

274

275 276

	// return information about user attributes
277
	public function get_user_attributes($force_reload = false)
278
	{
279 280 281
		if ($force_reload) {
			unset($this->user_attributes);
		}
282

rjsmelo's avatar
rjsmelo committed
283
		if (! empty($this->user_attributes)) {
284
			return $this->user_attributes;
285 286
		}

287
		$userdn = $this->user_dn();
288

289
		// ensure we have a connection to the ldap server
290 291 292
		if ($this->bind() !== LdapException::LDAP_SUCCESS) {
			//@todo fix this error since getMessage no longer works
			$this->add_log('ldap', 'Reuse of ldap connection failed: ' . $this->ldaplink->getLastError() . ' at line ' . __LINE__ . ' in ' . __FILE__);
293 294 295 296
			return false;
		}

		// todo: only fetch needed attributes
297

298
		//A non-existing user may not return ldaplink->getEntry (found bug on windows server), if not found, user input incorrect username/password
299 300 301 302 303 304
		try {
			$searchresult = $this->ldaplink->search("(objectClass=*)", $userdn, Ldap::SEARCH_SCOPE_BASE, [], null);
			$searchresult->getInnerIterator()->setAttributeNameTreatment(LdapCollectionIterator::ATTRIBUTE_NATIVE);
			$entry = $searchresult->getFirst();
		} catch (LdapException $e) {
			$entry = null;
305
		}
306

307
		if ($force_reload || is_null($entry)) { // wrong userdn. So we have to search
308
			// prepare Search Filter
309 310 311 312 313
			$filter = Filter::equals($this->options['userattr'], $this->options['username']);
			$this->add_log('ldap', 'Searching for user information with filter: ' . $filter->toString() . ' at line ' . __LINE__ . ' in ' . __FILE__);

			try {
				$searchresult = $this->ldaplink->search($filter, $this->userbase_dn(), $this->options['scope']);
314
				$searchresult->getInnerIterator()->setAttributeNameTreatment(LdapCollectionIterator::ATTRIBUTE_NATIVE);
315 316
			} catch (LdapException $e) {
				$this->add_log('ldap', 'Search failed: ' . $e->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
317 318
				return false;
			}
319

320 321
			if ($searchresult->count() != 1) {
				$this->add_log('ldap', 'Error: Search returned ' . $searchresult->count() . ' entries' . ' at line ' . __LINE__ . ' in ' . __FILE__);
322 323 324
				return false;
			}
			// get first entry
325
			$entry = $searchresult->getFirst();
326
		}
327

328 329 330
		$this->user_attributes = $this->parseLdapAttributes($entry);
		if (empty($this->user_attributes)) {
			$this->add_log('ldap', 'Error fetching user attributes at line ' . __LINE__ . ' in ' . __FILE__);
331 332 333
			return false;
		}

334
		return $this->user_attributes;
335 336
	} // End: public function get_user_attributes()

337 338 339 340
	// Request all users attributes
	public function get_all_users_attributes()
	{
		// ensure we have a connection to the ldap server
341 342
		if ($this->bind() !== LdapException::LDAP_SUCCESS) {
			$this->add_log('ldap', 'Reuse of ldap connection failed: ' . $this->ldaplink->getLastError() . ' at line ' . __LINE__ . ' in ' . __FILE__);
343 344 345 346
			return false;
		}

		// Prepare Search Filter
347
		$filter = Filter::equals('objectclass', $this->options['useroc']);
348

349
		$this->add_log('ldap', 'Searching for user information with filter: ' . $filter->toString() . ' at line ' . __LINE__ . ' in ' . __FILE__);
350

351 352
		try {
			$searchresult = $this->ldaplink->search($filter, $this->userbase_dn(), $this->options['scope']);
353
			$searchresult->getInnerIterator()->setAttributeNameTreatment(LdapCollectionIterator::ATTRIBUTE_NATIVE);
354 355
		} catch (LdapException $e) {
			$this->add_log('ldap', 'Search failed: ' . $e->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
356 357 358 359
			return false;
		}

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

364
		$entries = $searchresult->toArray();
rjsmelo's avatar
rjsmelo committed
365
		$users_attributes = [];
366 367

		foreach ($entries as $entry) {
368
			$users_attributes[] = $this->parseLdapAttributes($entry);
369
		}
370

371 372
		return ($users_attributes);
	} // End: public function get_user_attributes()
373 374

	// return dn of all groups a user belongs to
375
	public function get_groups($force_reload = false)
376
	{
377 378
		$this->get_user_attributes($force_reload);

379
		// ensure we have a connection to the ldap server
380 381
		if ($this->bind() !== LdapException::LDAP_SUCCESS) {
			$this->add_log('ldap', 'Reuse of ldap connection failed: ' . $this->ldaplink->getLastError() . ' at line ' . __LINE__ . ' in ' . __FILE__);
382 383
			return false;
		}
384

385
		$filter1 = Filter::equals('objectClass', $this->options['groupoc']);
386

rjsmelo's avatar
rjsmelo committed
387
		if (! empty($this->options['groupmemberattr'])) {
388
			// get membership from group information
389
			if ($this->options['groupmemberisdn']) {
390 391 392
				if ($this->user_attributes['dn'] == null) {
					return false;
				}
393
				$filter2 = Filter::equals($this->options['groupmemberattr'], $this->user_dn()) ;
394
			} else {
395
				$filter2 = Filter::equals($this->options['groupmemberattr'], $this->options['username']);
396
			}
397
			$filter = Filter::andFilter($filter1, $filter2);
rjsmelo's avatar
rjsmelo committed
398
		} elseif (! empty($this->options['usergroupattr'])) {
399
			// get membership from user information
400

401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424
			if ($this->options['usergroupattr'] === 'distinguishedName') {
				// get membership from user DN

				// split DN into RDN strings
				$dn_string = $this->user_attributes[$this->options['usergroupattr']];
				$rdn_strings = explode(',', $dn_string);

				// add value of RDNs with OU type
				$ugi = [];
				foreach ($rdn_strings as $rdn_string) {
					// split RDN string in type and value
					$rdn_parts = explode('=', $rdn_string, 2);
					$rdn_type = $rdn_parts[0];
					$rdn_value = $rdn_parts[1];

					// add RDN value if type is OU
					if (strtoupper($rdn_type) === 'OU') {
						$ugi[] = $rdn_value;
					}
				}
			} else {
				$ugi = &$this->user_attributes[$this->options['usergroupattr']];
			}

rjsmelo's avatar
rjsmelo committed
425 426 427
			if (! empty($ugi)) {
				if (! is_array($ugi)) {
					$ugi = [$ugi];
428
				}
429

430
				if (count($ugi) == 1) { // one gid
431
					$filter3 = Filter::equals($this->options['groupgroupattr'], $ugi[0]);
432
				} else { // mor gids
rjsmelo's avatar
rjsmelo committed
433
					$filtertmp = [];
434
					foreach ($ugi as $g) {
435
						$filtertmp[] = Filter::equals($this->options['groupgroupattr'], $g);
436
					}
437 438

					$filter3 = call_user_func_array('Filter::orFilter', $filtertmp);
439
				}
440

441
				$filter = Filter::andFilter($filter1, $filter3);
442
			} else { // User has no group
443
				return [];
444
			}
445 446
		} else {
			// not possible to get groups - return empty array
447
			return [];
448 449
		}

450
		$this->add_log(
451
			'ldap',
452
			'Searching for group entries with filter: ' . $filter->toString() . ' base ' .
453
			$this->groupbase_dn() . ' at line ' . __LINE__ . ' in ' . __FILE__
454 455
		);

456 457
		try {
			$searchresult = $this->ldaplink->search($filter, $this->groupbase_dn(), $this->options['scope']);
458
			$searchresult->getInnerIterator()->setAttributeNameTreatment(LdapCollectionIterator::ATTRIBUTE_NATIVE);
459 460
		} catch (LdapException $e) {
			$this->add_log('ldap', 'Search failed: ' . $e->getMessage() . ' at line ' . __LINE__ . ' in ' . __FILE__);
461 462
			return false;
		}
463

464 465
		$this->add_log('ldap', 'Found ' . $searchresult->count() . ' entries. Extracting entries now.');

466
		$groupEntries = $searchresult->toArray();
rjsmelo's avatar
rjsmelo committed
467
		$this->groups = [];
468 469 470 471

		foreach ($groupEntries as $entry) {
			if (empty($entry)) {
				continue;
472
			}
473 474 475

			$group = $this->parseLdapAttributes($entry);
			$this->groups[$group['dn']] = $group; // no error checking necessary here
476
		}
477
		$this->add_log('ldap', count($this->groups) . ' groups found at line ' . __LINE__ . ' in ' . __FILE__);
478 479 480 481 482 483 484

		return($this->groups);
	} // End: private function get_group_dns()




485 486 487
	// helper functions
	private function userbase_dn()
	{
rjsmelo's avatar
rjsmelo committed
488
		if (empty($this->options['userdn'])) {
489
			return($this->options['baseDn']);
rjsmelo's avatar
rjsmelo committed
490
		}
491
		return($this->options['userdn'] . ',' . $this->options['baseDn']);
492 493
	}

494 495 496
	private function user_dn()
	{
		if (isset($this->user_attributes['dn'])) {
497 498 499
			// we did already fetch user attributes and have the real dn now
			return($this->user_attributes['dn']);
		}
500 501
		if (empty($this->options['userattr'])) {
			$ua = 'cn=';
502
		} else {
503 504
			$ua = $this->options['userattr'] . '=';
		}
rjsmelo's avatar
rjsmelo committed
505
		return($ua . $this->options['username'] . ',' . $this->userbase_dn());
506 507
	}

508 509
	private function groupbase_dn()
	{
rjsmelo's avatar
rjsmelo committed
510
		if (empty($this->options['groupdn'])) {
511
			return($this->options['baseDn']);
rjsmelo's avatar
rjsmelo committed
512
		}
513
		return($this->options['groupdn'] . ',' . $this->options['baseDn']);
514 515
	}

516 517
	private function add_log($facility, $message)
	{
rjsmelo's avatar
rjsmelo committed
518
		if ($this->options['debug']) {
519
			$this->logslib->add_log($facility, $message);
rjsmelo's avatar
rjsmelo committed
520
		}
521 522
	}

523
	/**
524
	 * Setter to set an option value
525 526 527 528 529
	 * @param string $name The name of the option
	 * @param mixed $value The value
	 * @return void
	 * @throw Exception
	 */
rjsmelo's avatar
rjsmelo committed
530
	public function setOption($name, $value = null)
531
	{
532 533 534 535 536
		if (isset($this->options[$name])) {
			$this->options[$name] = $value;
		} else {
			throw new Exception(sprintf("Undefined option: %s \n", $name), E_USER_WARNING);
		}
537 538 539
	}

	/**
marclaporte's avatar
marclaporte committed
540
	 * Return the value of the attribute past in param
541 542 543 544
	 * @param string $name The name of the attribute
	 * @return mixed
	 * @throw Exception
	 */
rjsmelo's avatar
rjsmelo committed
545
	public function getUserAttribute($name)
546
	{
547 548 549 550 551 552 553 554
		$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);
			}
555 556
		} catch (Exception $e) {
		}
557
		return $value;
558
	}
559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578

	/**
	 * Parse the ldap retrieved attributes for a given entry
	 *
	 * @param $entry
	 * @return array
	 */
	private function parseLdapAttributes($entry)
	{
		$attributes = [];
		foreach ($entry as $key => $value) {
			if (is_array($value)) {
				$attributes[$key] = count($value) == 1 ? array_shift($value) : $value;
			} else {
				$attributes[$key] = $value;
			}
		}

		return $attributes;
	}
579
}