Overview

Packages

  • Auth

Classes

  • Horde_Auth
  • Horde_Auth_Auto
  • Horde_Auth_Base
  • Horde_Auth_Composite
  • Horde_Auth_Customsql
  • Horde_Auth_Cyrsql
  • Horde_Auth_Exception
  • Horde_Auth_Ftp
  • Horde_Auth_Http
  • Horde_Auth_Http_Remote
  • Horde_Auth_Imap
  • Horde_Auth_Ipbasic
  • Horde_Auth_Kolab
  • Horde_Auth_Ldap
  • Horde_Auth_Login
  • Horde_Auth_Msad
  • Horde_Auth_Pam
  • Horde_Auth_Passwd
  • Horde_Auth_Peclsasl
  • Horde_Auth_Radius
  • Horde_Auth_Shibboleth
  • Horde_Auth_Smb
  • Horde_Auth_Smbclient
  • Horde_Auth_Sql
  • Horde_Auth_Translation
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * The Horde_Auth_Ldap class provides an LDAP implementation of the Horde
  4:  * authentication system.
  5:  *
  6:  * 'preauthenticate' hook should return LDAP connection information in the
  7:  * 'ldap' credentials key.
  8:  *
  9:  * Copyright 1999-2012 Horde LLC (http://www.horde.org/)
 10:  *
 11:  * See the enclosed file COPYING for license information (LGPL). If you did
 12:  * not receive this file, http://www.horde.org/licenses/lgpl21
 13:  *
 14:  * @author   Jon Parise <jon@horde.org>
 15:  * @category Horde
 16:  * @license http://www.horde.org/licenses/lgpl21 LGPL-2.1
 17:  * @package  Auth
 18:  */
 19: class Horde_Auth_Ldap extends Horde_Auth_Base
 20: {
 21:     /**
 22:      * An array of capabilities, so that the driver can report which
 23:      * operations it supports and which it doesn't.
 24:      *
 25:      * @var array
 26:      */
 27:     protected $_capabilities = array(
 28:         'add' => true,
 29:         'update' => true,
 30:         'resetpassword' => true,
 31:         'remove' => true,
 32:         'list' => true,
 33:         'authenticate' => true,
 34:     );
 35: 
 36:     /**
 37:      * LDAP object
 38:      *
 39:      * @var Horde_Ldap
 40:      */
 41:     protected $_ldap;
 42: 
 43:     /**
 44:      * Constructor.
 45:      *
 46:      * @param array $params  Required parameters:
 47:      * <pre>
 48:      * 'basedn' - (string) [REQUIRED] The base DN for the LDAP server.
 49:      * 'filter' - (string) The LDAP formatted search filter to search for
 50:      *            users. This setting overrides the 'objectclass' parameter.
 51:      * 'ldap' - (Horde_Ldap) [REQUIRED] Horde LDAP object.
 52:      * 'objectclass - (string|array): The objectclass filter used to search
 53:      *                for users. Either a single or an array of objectclasses.
 54:      * 'uid' - (string) [REQUIRED] The username search key.
 55:      * </pre>
 56:      *
 57:      * @throws Horde_Auth_Exception
 58:      * @throws InvalidArgumentException
 59:      */
 60:     public function __construct(array $params = array())
 61:     {
 62:         foreach (array('basedn', 'ldap', 'uid') as $val) {
 63:             if (!isset($params[$val])) {
 64:                 throw new InvalidArgumentException(__CLASS__ . ': Missing ' . $val . ' parameter.');
 65:             }
 66:         }
 67: 
 68:         if (!empty($this->_params['ad'])) {
 69:             $this->_capabilities['resetpassword'] = false;
 70:         }
 71: 
 72:         $this->_ldap = $params['ldap'];
 73:         unset($params['ldap']);
 74: 
 75:         parent::__construct($params);
 76:     }
 77: 
 78:     /**
 79:      * Checks for shadowLastChange and shadowMin/Max support and returns their
 80:      * values.  We will also check for pwdLastSet if Active Directory is
 81:      * support is requested.  For this check to succeed we need to be bound
 82:      * to the directory.
 83:      *
 84:      * @param string $dn  The dn of the user.
 85:      *
 86:      * @return array  Array with keys being "shadowlastchange", "shadowmin"
 87:      *                "shadowmax", "shadowwarning" and containing their
 88:      *                respective values or false for no support.
 89:      */
 90:     protected function _lookupShadow($dn)
 91:     {
 92:         /* Init the return array. */
 93:         $lookupshadow = array(
 94:             'shadowlastchange' => false,
 95:             'shadowmin' => false,
 96:             'shadowmax' => false,
 97:             'shadowwarning' => false
 98:         );
 99: 
100:         /* According to LDAP standard, to read operational attributes, you
101:          * must request them explicitly. Attributes involved in password
102:          * expiration policy:
103:          *    pwdlastset: Active Directory
104:          *    shadow*: shadowUser schema
105:          *    passwordexpirationtime: Sun and Fedora Directory Server */
106:         try {
107:             $result = $this->_ldap->search(null, '(objectClass=*)', array(
108:                 'attributes' => array(
109:                     'pwdlastset',
110:                     'shadowmax',
111:                     'shadowmin',
112:                     'shadowlastchange',
113:                     'shadowwarning',
114:                     'passwordexpirationtime'
115:                 ),
116:                 'scope' => 'base'
117:             ));
118:         } catch (Horde_Ldap_Exception $e) {
119:             return $lookupshadow;
120:         }
121: 
122:         if (!$result) {
123:             return $lookupshadow;
124:         }
125: 
126:         $info = reset($result);
127: 
128:         // TODO: 'ad'?
129:         if (!empty($this->_params['ad'])) {
130:             if (isset($info['pwdlastset'][0])) {
131:                 /* Active Directory handles timestamps a bit differently.
132:                  * Convert the timestamp to a UNIX timestamp. */
133:                 $lookupshadow['shadowlastchange'] = floor((($info['pwdlastset'][0] / 10000000) - 11644406783) / 86400) - 1;
134: 
135:                 /* Password expiry attributes are in a policy. We cannot
136:                  * read them so use the Horde config. */
137:                 $lookupshadow['shadowwarning'] = $this->_params['warnage'];
138:                 $lookupshadow['shadowmin'] = $this->_params['minage'];
139:                 $lookupshadow['shadowmax'] = $this->_params['maxage'];
140:             }
141:         } elseif (isset($info['passwordexpirationtime'][0])) {
142:             /* Sun/Fedora Directory Server uses a special attribute
143:              * passwordexpirationtime.  It has precedence over shadow*
144:              * because it actually locks the expired password at the LDAP
145:              * server level.  The correct way to check expiration should
146:              * be using LDAP controls, unfortunately PHP doesn't support
147:              * controls on bind() responses. */
148:             $ldaptimepattern = "/([0-9]{4})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})Z/";
149:             if (preg_match($ldaptimepattern, $info['passwordexpirationtime'][0], $regs)) {
150:                 /* Sun/Fedora Directory Server return expiration time, not
151:                  * last change time. We emulate the behaviour taking it
152:                  * back to maxage. */
153:                 $lookupshadow['shadowlastchange'] = floor(mktime($regs[4], $regs[5], $regs[6], $regs[2], $regs[3], $regs[1]) / 86400) - $this->_params['maxage'];
154: 
155:                 /* Password expiry attributes are in not accessible policy
156:                  * entry. */
157:                 $lookupshadow['shadowwarning'] = $this->_params['warnage'];
158:                 $lookupshadow['shadowmin']     = $this->_params['minage'];
159:                 $lookupshadow['shadowmax']     = $this->_params['maxage'];
160:             } elseif ($this->_logger) {
161:                 $this->_logger->log('Wrong time format: ' . $info['passwordexpirationtime'][0], 'ERR');
162:             }
163:         } else {
164:             if (isset($info['shadowmax'][0])) {
165:                 $lookupshadow['shadowmax'] = $info['shadowmax'][0];
166:             }
167:             if (isset($info['shadowmin'][0])) {
168:                 $lookupshadow['shadowmin'] = $info['shadowmin'][0];
169:             }
170:             if (isset($info['shadowlastchange'][0])) {
171:                 $lookupshadow['shadowlastchange'] = $info['shadowlastchange'][0];
172:             }
173:             if (isset($info['shadowwarning'][0])) {
174:                 $lookupshadow['shadowwarning'] = $info['shadowwarning'][0];
175:             }
176:         }
177: 
178:         return $lookupshadow;
179:     }
180: 
181:     /**
182:      * Find out if the given set of login credentials are valid.
183:      *
184:      * @param string $userId       The userId to check.
185:      * @param array  $credentials  An array of login credentials.
186:      *
187:      * @throws Horde_Auth_Exception
188:      */
189:     protected function _authenticate($userId, $credentials)
190:     {
191:         /* Search for the user's full DN. */
192:         $this->_ldap->bind();
193:         try {
194:             $dn = $this->_ldap->findUserDN($userId);
195:         } catch (Horde_Exception_NotFound $e) {
196:             throw new Horde_Auth_Exception('', Horde_Auth::REASON_BADLOGIN);
197:         } catch (Horde_Exception_Ldap $e) {
198:             throw new Horde_Auth_Exception($e->getMessage(), Horde_Auth::REASON_MESSAGE);
199:         }
200: 
201:         /* Attempt to bind to the LDAP server as the user. */
202:         try {
203:             $this->_ldap->bind($dn, $credentials['password']);
204:         } catch (Horde_Ldap_Exception $e) {
205:             if (Horde_Ldap::errorName($e->getCode() == 'LDAP_INVALID_CREDENTIALS')) {
206:                 throw new Horde_Auth_Exception('', Horde_Auth::REASON_BADLOGIN);
207:             }
208:             throw new Horde_Auth_Exception($e->getMessage(), Horde_Auth::REASON_MESSAGE);
209:         }
210: 
211:         if ($this->_params['password_expiration'] == 'yes') {
212:             $shadow = $this->_lookupShadow($dn);
213:             if ($shadow['shadowmax'] && $shadow['shadowlastchange'] &&
214:                 $shadow['shadowwarning']) {
215:                 $today = floor(time() / 86400);
216:                 $toexpire = $shadow['shadowlastchange'] +
217:                             $shadow['shadowmax'] - $today;
218: 
219:                 $warnday = $shadow['shadowlastchange'] + $shadow['shadowmax'] - $shadow['shadowwarning'];
220:                 if ($today >= $warnday) {
221:                     $this->setCredential('expire', $toexpire);
222:                 }
223: 
224:                 if ($toexpire == 0) {
225:                     $this->setCredential('change', true);
226:                 } elseif ($toexpire < 0) {
227:                     throw new Horde_Auth_Exception('', Horde_Auth::REASON_EXPIRED);
228:                 }
229:             }
230:         }
231:     }
232: 
233:     /**
234:      * Add a set of authentication credentials.
235:      *
236:      * @param string $userId      The userId to add.
237:      * @param array $credentials  The credentials to be set.
238:      *
239:      * @throws Horde_Auth_Exception
240:      */
241:     public function addUser($userId, $credentials)
242:     {
243:         if (!empty($this->_params['ad'])) {
244:             throw new Horde_Auth_Exception(__CLASS__ . ': Adding users is not supported for Active Directory.');
245:         }
246: 
247:         if (isset($credentials['ldap'])) {
248:             $entry = $credentials['ldap'];
249:             $dn = $entry['dn'];
250: 
251:             /* Remove the dn entry from the array. */
252:             unset($entry['dn']);
253:         } else {
254:             /* Try this simple default and hope it works. */
255:             $dn = $this->_params['uid'] . '=' . $userId . ','
256:                 . $this->_params['basedn'];
257:             $entry['cn'] = $userId;
258:             $entry['sn'] = $userId;
259:             $entry[$this->_params['uid']] = $userId;
260:             $entry['objectclass'] = array_merge(
261:                 array('top'),
262:                 $this->_params['newuser_objectclass']);
263:             $entry['userPassword'] = Horde_Auth::getCryptedPassword(
264:                 $credentials['password'], '',
265:                 $this->_params['encryption'],
266:                 'true');
267: 
268:             if ($this->_params['password_expiration'] == 'yes') {
269:                 $entry['shadowMin'] = $this->_params['minage'];
270:                 $entry['shadowMax'] = $this->_params['maxage'];
271:                 $entry['shadowWarning'] = $this->_params['warnage'];
272:                 $entry['shadowLastChange'] = floor(time() / 86400);
273:             }
274:         }
275: 
276:         try {
277:             $this->_ldap->add(Horde_Ldap_Entry::createFresh($dn, $entry));
278:         } catch (Horde_Ldap_Exception $e) {
279:             throw new Horde_Auth_Exception(sprintf(__CLASS__ . ': Unable to add user "%s". This is what the server said: ', $userId) . $e->getMessage());
280:         }
281:     }
282: 
283:     /**
284:      * Remove a set of authentication credentials.
285:      *
286:      * @param string $userId  The userId to add.
287:      * @param string $dn      TODO
288:      *
289:      * @throws Horde_Auth_Exception
290:      */
291:     public function removeUser($userId, $dn = null)
292:     {
293:         if (!empty($this->_params['ad'])) {
294:             throw new Horde_Auth_Exception(__CLASS__ . ': Removing users is not supported for Active Directory');
295:         }
296: 
297:         if (is_null($dn)) {
298:             /* Search for the user's full DN. */
299:             try {
300:                 $dn = $this->_ldap->findUserDN($userId);
301:             } catch (Horde_Exception_Ldap $e) {
302:                 throw new Horde_Auth_Exception($e);
303:             }
304:         }
305: 
306:         try {
307:             $this->_ldap->delete($dn);
308:         } catch (Horde_Ldap_Exception $e) {
309:             throw new Horde_Auth_Exception(sprintf(__CLASS__ . ': Unable to remove user "%s"', $userId));
310:         }
311:     }
312: 
313:     /**
314:      * Update a set of authentication credentials.
315:      *
316:      * @todo Clean this up for Horde 5.
317:      *
318:      * @param string $oldID       The old userId.
319:      * @param string $newID       The new userId.
320:      * @param array $credentials  The new credentials.
321:      * @param string $olddn       The old user DN.
322:      * @param string $newdn       The new user DN.
323:      *
324:      * @throws Horde_Auth_Exception
325:      */
326:     public function updateUser($oldID, $newID, $credentials, $olddn = null,
327:                                $newdn = null)
328:     {
329:         if (!empty($this->_params['ad'])) {
330:             throw new Horde_Auth_Exception(__CLASS__ . ': Updating users is not supported for Active Directory.');
331:         }
332: 
333:         if (is_null($olddn)) {
334:             /* Search for the user's full DN. */
335:             try {
336:                 $dn = $this->_ldap->findUserDN($oldID);
337:             } catch (Horde_Exception_Ldap $e) {
338:                 throw new Horde_Auth_Exception($e);
339:             }
340: 
341:             $olddn = $dn;
342:             $newdn = preg_replace('/uid=.*?,/', 'uid=' . $newID . ',', $dn, 1);
343:             $shadow = $this->_lookupShadow($dn);
344: 
345:             /* If shadowmin hasn't yet expired only change when we are
346:                administrator */
347:             if ($shadow['shadowlastchange'] &&
348:                 $shadow['shadowmin'] &&
349:                 ($shadow['shadowlastchange'] + $shadow['shadowmin'] > (time() / 86400))) {
350:                 throw new Horde_Auth_Exception('Minimum password age has not yet expired');
351:             }
352: 
353:             /* Set the lastchange field */
354:             if ($shadow['shadowlastchange']) {
355:                 $entry['shadowlastchange'] =  floor(time() / 86400);
356:             }
357: 
358:             /* Encrypt the new password */
359:             $entry['userpassword'] = Horde_Auth::getCryptedPassword(
360:                 $credentials['password'], '',
361:                 $this->_params['encryption'],
362:                 'true');
363:         } else {
364:             $entry = $credentials;
365:             unset($entry['dn']);
366:         }
367: 
368:         try {
369:             if ($oldID != $newID) {
370:                 $this->_ldap->move($olddn, $newdn);
371:                 $this->_ldap->modify($newdn, $entry);
372:             } else {
373:                 $this->_ldap->modify($olddn, $entry);
374:             }
375:         } catch (Horde_Ldap_Exception $e) {
376:             throw new Horde_Auth_Exception(sprintf(__CLASS__ . ': Unable to update user "%s"', $newID));
377:         }
378:     }
379: 
380:     /**
381:      * Reset a user's password. Used for example when the user does not
382:      * remember the existing password.
383:      *
384:      * @param string $userId  The user id for which to reset the password.
385:      *
386:      * @return string  The new password on success.
387:      * @throws Horde_Auth_Exception
388:      */
389:     public function resetPassword($userId)
390:     {
391:         if (!empty($this->_params['ad'])) {
392:             throw new Horde_Auth_Exception(__CLASS__ . ': Updating users is not supported for Active Directory.');
393:         }
394: 
395:         /* Search for the user's full DN. */
396:         try {
397:             $dn = $this->_ldap->findUserDN($userId);
398:         } catch (Horde_Exception_Ldap $e) {
399:             throw new Horde_Auth_Exception($e);
400:         }
401: 
402:         /* Get a new random password. */
403:         $password = Horde_Auth::genRandomPassword();
404: 
405:         /* Encrypt the new password */
406:         $entry = array(
407:             'userpassword' => Horde_Auth::getCryptedPassword($password,
408:                                                              '',
409:                                                              $this->_params['encryption'],
410:                                                              'true'));
411: 
412:         /* Set the lastchange field */
413:         $shadow = $this->_lookupShadow($dn);
414:         if ($shadow['shadowlastchange']) {
415:             $entry['shadowlastchange'] = floor(time() / 86400);
416:         }
417: 
418:         /* Update user entry. */
419:         try {
420:             $this->_ldap->modify($dn, $entry);
421:         } catch (Horde_Ldap_Exception $e) {
422:             throw new Horde_Auth_Exception($e);
423:         }
424: 
425:         return $password;
426:     }
427: 
428:     /**
429:      * List Users
430:      *
431:      * @return array  List of Users
432:      * @throws Horde_Auth_Exception
433:      */
434:     public function listUsers($sort = false)
435:     {
436:         $params = array(
437:             'attributes' => array($this->_params['uid']),
438:             'scope' => $this->_params['scope'],
439:             'sizelimit' => isset($this->_params['sizelimit']) ? $this->_params['sizelimit'] : 0
440:         );
441: 
442:         /* Add a sizelimit, if specified. Default is 0, which means no limit.
443:          * Note: You cannot override a server-side limit with this. */
444:         $userlist = array();
445:         try {
446:             $search = $this->_ldap->search(
447:                 $this->_params['basedn'],
448:                 Horde_Ldap_Filter::build(array('filter' => $this->_params['filter'])),
449:                 $params);
450:             $uid = Horde_String::lower($this->_params['uid']);
451:             foreach ($search as $val) {
452:                 $userlist[] = $val->getValue($uid, 'single');
453:             }
454:         } catch (Horde_Ldap_Exception $e) {}
455:         return $this->_sort($userlist, $sort);
456:     }
457: 
458:     /**
459:      * Checks if $userId exists in the LDAP backend system.
460:      *
461:      * @author Marco Ferrante, University of Genova (I)
462:      *
463:      * @param string $userId  User ID for which to check
464:      *
465:      * @return boolean  Whether or not $userId already exists.
466:      */
467:     public function exists($userId)
468:     {
469:         $params = array(
470:             'scope' => $this->_params['scope']
471:         );
472: 
473:         try {
474:             $uidfilter = Horde_Ldap_Filter::create($this->_params['uid'], 'equals', $userId);
475:             $classfilter = Horde_Ldap_Filter::build(array('filter' => $this->_params['filter']));
476: 
477:             $search = $this->_ldap->search(
478:                 $this->_params['basedn'],
479:                 Horde_Ldap_Filter::combine('and', array($uidfilter, $classfilter)),
480:                 $params);
481:             if ($search->count() < 1) {
482:                 return false;
483:             }
484:             if ($search->count() > 1 && $this->_logger) {
485:                 $this->_logger->log('Multiple LDAP entries with user identifier ' . $userId, 'WARN');
486:             }
487:             return true;
488:         } catch (Horde_Ldap_Exception $e) {
489:             if ($this->_logger) {
490:                 $this->_logger->log('Error searching LDAP user: ' . $e->getMessage(), 'ERR');
491:             }
492:             return false;
493:         }
494:     }
495: }
496: 
API documentation generated by ApiGen