web/lib/Zend/Auth/Adapter/Ldap.php
changeset 64 162c1de6545a
parent 19 1c2f13fd785c
child 68 ecaf28ffe26e
equal deleted inserted replaced
63:5b37998e522e 64:162c1de6545a
       
     1 <?php
       
     2 /**
       
     3  * Zend Framework
       
     4  *
       
     5  * LICENSE
       
     6  *
       
     7  * This source file is subject to the new BSD license that is bundled
       
     8  * with this package in the file LICENSE.txt.
       
     9  * It is also available through the world-wide-web at this URL:
       
    10  * http://framework.zend.com/license/new-bsd
       
    11  * If you did not receive a copy of the license and are unable to
       
    12  * obtain it through the world-wide-web, please send an email
       
    13  * to license@zend.com so we can send you a copy immediately.
       
    14  *
       
    15  * @category   Zend
       
    16  * @package    Zend_Auth
       
    17  * @subpackage Zend_Auth_Adapter
       
    18  * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
       
    19  * @license    http://framework.zend.com/license/new-bsd     New BSD License
       
    20  * @version    $Id: Ldap.php 21319 2010-03-04 16:02:16Z sgehrig $
       
    21  */
       
    22 
       
    23 /**
       
    24  * @see Zend_Auth_Adapter_Interface
       
    25  */
       
    26 require_once 'Zend/Auth/Adapter/Interface.php';
       
    27 
       
    28 /**
       
    29  * @category   Zend
       
    30  * @package    Zend_Auth
       
    31  * @subpackage Zend_Auth_Adapter
       
    32  * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
       
    33  * @license    http://framework.zend.com/license/new-bsd     New BSD License
       
    34  */
       
    35 class Zend_Auth_Adapter_Ldap implements Zend_Auth_Adapter_Interface
       
    36 {
       
    37 
       
    38     /**
       
    39      * The Zend_Ldap context.
       
    40      *
       
    41      * @var Zend_Ldap
       
    42      */
       
    43     protected $_ldap = null;
       
    44 
       
    45     /**
       
    46      * The array of arrays of Zend_Ldap options passed to the constructor.
       
    47      *
       
    48      * @var array
       
    49      */
       
    50     protected $_options = null;
       
    51 
       
    52     /**
       
    53      * The username of the account being authenticated.
       
    54      *
       
    55      * @var string
       
    56      */
       
    57     protected $_username = null;
       
    58 
       
    59     /**
       
    60      * The password of the account being authenticated.
       
    61      *
       
    62      * @var string
       
    63      */
       
    64     protected $_password = null;
       
    65 
       
    66     /**
       
    67      * The DN of the authenticated account. Used to retrieve the account entry on request.
       
    68      *
       
    69      * @var string
       
    70      */
       
    71     protected $_authenticatedDn = null;
       
    72 
       
    73     /**
       
    74      * Constructor
       
    75      *
       
    76      * @param  array  $options  An array of arrays of Zend_Ldap options
       
    77      * @param  string $username The username of the account being authenticated
       
    78      * @param  string $password The password of the account being authenticated
       
    79      * @return void
       
    80      */
       
    81     public function __construct(array $options = array(), $username = null, $password = null)
       
    82     {
       
    83         $this->setOptions($options);
       
    84         if ($username !== null) {
       
    85             $this->setUsername($username);
       
    86         }
       
    87         if ($password !== null) {
       
    88             $this->setPassword($password);
       
    89         }
       
    90     }
       
    91 
       
    92     /**
       
    93      * Returns the array of arrays of Zend_Ldap options of this adapter.
       
    94      *
       
    95      * @return array|null
       
    96      */
       
    97     public function getOptions()
       
    98     {
       
    99         return $this->_options;
       
   100     }
       
   101 
       
   102     /**
       
   103      * Sets the array of arrays of Zend_Ldap options to be used by
       
   104      * this adapter.
       
   105      *
       
   106      * @param  array $options The array of arrays of Zend_Ldap options
       
   107      * @return Zend_Auth_Adapter_Ldap Provides a fluent interface
       
   108      */
       
   109     public function setOptions($options)
       
   110     {
       
   111         $this->_options = is_array($options) ? $options : array();
       
   112         return $this;
       
   113     }
       
   114 
       
   115     /**
       
   116      * Returns the username of the account being authenticated, or
       
   117      * NULL if none is set.
       
   118      *
       
   119      * @return string|null
       
   120      */
       
   121     public function getUsername()
       
   122     {
       
   123         return $this->_username;
       
   124     }
       
   125 
       
   126     /**
       
   127      * Sets the username for binding
       
   128      *
       
   129      * @param  string $username The username for binding
       
   130      * @return Zend_Auth_Adapter_Ldap Provides a fluent interface
       
   131      */
       
   132     public function setUsername($username)
       
   133     {
       
   134         $this->_username = (string) $username;
       
   135         return $this;
       
   136     }
       
   137 
       
   138     /**
       
   139      * Returns the password of the account being authenticated, or
       
   140      * NULL if none is set.
       
   141      *
       
   142      * @return string|null
       
   143      */
       
   144     public function getPassword()
       
   145     {
       
   146         return $this->_password;
       
   147     }
       
   148 
       
   149     /**
       
   150      * Sets the passwort for the account
       
   151      *
       
   152      * @param  string $password The password of the account being authenticated
       
   153      * @return Zend_Auth_Adapter_Ldap Provides a fluent interface
       
   154      */
       
   155     public function setPassword($password)
       
   156     {
       
   157         $this->_password = (string) $password;
       
   158         return $this;
       
   159     }
       
   160 
       
   161     /**
       
   162      * setIdentity() - set the identity (username) to be used
       
   163      *
       
   164      * Proxies to {@see setUsername()}
       
   165      *
       
   166      * Closes ZF-6813
       
   167      *
       
   168      * @param  string $identity
       
   169      * @return Zend_Auth_Adapter_Ldap Provides a fluent interface
       
   170      */
       
   171     public function setIdentity($identity)
       
   172     {
       
   173         return $this->setUsername($identity);
       
   174     }
       
   175 
       
   176     /**
       
   177      * setCredential() - set the credential (password) value to be used
       
   178      *
       
   179      * Proxies to {@see setPassword()}
       
   180      *
       
   181      * Closes ZF-6813
       
   182      *
       
   183      * @param  string $credential
       
   184      * @return Zend_Auth_Adapter_Ldap Provides a fluent interface
       
   185      */
       
   186     public function setCredential($credential)
       
   187     {
       
   188         return $this->setPassword($credential);
       
   189     }
       
   190 
       
   191     /**
       
   192      * Returns the LDAP Object
       
   193      *
       
   194      * @return Zend_Ldap The Zend_Ldap object used to authenticate the credentials
       
   195      */
       
   196     public function getLdap()
       
   197     {
       
   198         if ($this->_ldap === null) {
       
   199             /**
       
   200              * @see Zend_Ldap
       
   201              */
       
   202             require_once 'Zend/Ldap.php';
       
   203             $this->_ldap = new Zend_Ldap();
       
   204         }
       
   205 
       
   206         return $this->_ldap;
       
   207     }
       
   208 
       
   209     /**
       
   210      * Set an Ldap connection
       
   211      *
       
   212      * @param Zend_Ldap $ldap An existing Ldap object
       
   213      * @return Zend_Auth_Adapter_Ldap Provides a fluent interface
       
   214      */
       
   215     public function setLdap(Zend_Ldap $ldap)
       
   216     {
       
   217         $this->_ldap = $ldap;
       
   218 
       
   219         $this->setOptions(array($ldap->getOptions()));
       
   220 
       
   221         return $this;
       
   222     }
       
   223 
       
   224     /**
       
   225      * Returns a domain name for the current LDAP options. This is used
       
   226      * for skipping redundant operations (e.g. authentications).
       
   227      *
       
   228      * @return string
       
   229      */
       
   230     protected function _getAuthorityName()
       
   231     {
       
   232         $options = $this->getLdap()->getOptions();
       
   233         $name = $options['accountDomainName'];
       
   234         if (!$name)
       
   235             $name = $options['accountDomainNameShort'];
       
   236         return $name ? $name : '';
       
   237     }
       
   238 
       
   239     /**
       
   240      * Authenticate the user
       
   241      *
       
   242      * @throws Zend_Auth_Adapter_Exception
       
   243      * @return Zend_Auth_Result
       
   244      */
       
   245     public function authenticate()
       
   246     {
       
   247         /**
       
   248          * @see Zend_Ldap_Exception
       
   249          */
       
   250         require_once 'Zend/Ldap/Exception.php';
       
   251 
       
   252         $messages = array();
       
   253         $messages[0] = ''; // reserved
       
   254         $messages[1] = ''; // reserved
       
   255 
       
   256         $username = $this->_username;
       
   257         $password = $this->_password;
       
   258 
       
   259         if (!$username) {
       
   260             $code = Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND;
       
   261             $messages[0] = 'A username is required';
       
   262             return new Zend_Auth_Result($code, '', $messages);
       
   263         }
       
   264         if (!$password) {
       
   265             /* A password is required because some servers will
       
   266              * treat an empty password as an anonymous bind.
       
   267              */
       
   268             $code = Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID;
       
   269             $messages[0] = 'A password is required';
       
   270             return new Zend_Auth_Result($code, '', $messages);
       
   271         }
       
   272 
       
   273         $ldap = $this->getLdap();
       
   274 
       
   275         $code = Zend_Auth_Result::FAILURE;
       
   276         $messages[0] = "Authority not found: $username";
       
   277         $failedAuthorities = array();
       
   278 
       
   279         /* Iterate through each server and try to authenticate the supplied
       
   280          * credentials against it.
       
   281          */
       
   282         foreach ($this->_options as $name => $options) {
       
   283 
       
   284             if (!is_array($options)) {
       
   285                 /**
       
   286                  * @see Zend_Auth_Adapter_Exception
       
   287                  */
       
   288                 require_once 'Zend/Auth/Adapter/Exception.php';
       
   289                 throw new Zend_Auth_Adapter_Exception('Adapter options array not an array');
       
   290             }
       
   291             $adapterOptions = $this->_prepareOptions($ldap, $options);
       
   292             $dname = '';
       
   293 
       
   294             try {
       
   295                 if ($messages[1])
       
   296                     $messages[] = $messages[1];
       
   297                 $messages[1] = '';
       
   298                 $messages[] = $this->_optionsToString($options);
       
   299 
       
   300                 $dname = $this->_getAuthorityName();
       
   301                 if (isset($failedAuthorities[$dname])) {
       
   302                     /* If multiple sets of server options for the same domain
       
   303                      * are supplied, we want to skip redundant authentications
       
   304                      * where the identity or credentials where found to be
       
   305                      * invalid with another server for the same domain. The
       
   306                      * $failedAuthorities array tracks this condition (and also
       
   307                      * serves to supply the original error message).
       
   308                      * This fixes issue ZF-4093.
       
   309                      */
       
   310                     $messages[1] = $failedAuthorities[$dname];
       
   311                     $messages[] = "Skipping previously failed authority: $dname";
       
   312                     continue;
       
   313                 }
       
   314 
       
   315                 $canonicalName = $ldap->getCanonicalAccountName($username);
       
   316                 $ldap->bind($canonicalName, $password);
       
   317                 /*
       
   318                  * Fixes problem when authenticated user is not allowed to retrieve
       
   319                  * group-membership information or own account.
       
   320                  * This requires that the user specified with "username" and optionally
       
   321                  * "password" in the Zend_Ldap options is able to retrieve the required
       
   322                  * information.
       
   323                  */
       
   324                 $requireRebind = false;
       
   325                 if (isset($options['username'])) {
       
   326                     $ldap->bind();
       
   327                     $requireRebind = true;
       
   328                 }
       
   329                 $dn = $ldap->getCanonicalAccountName($canonicalName, Zend_Ldap::ACCTNAME_FORM_DN);
       
   330 
       
   331                 $groupResult = $this->_checkGroupMembership($ldap, $canonicalName, $dn, $adapterOptions);
       
   332                 if ($groupResult === true) {
       
   333                     $this->_authenticatedDn = $dn;
       
   334                     $messages[0] = '';
       
   335                     $messages[1] = '';
       
   336                     $messages[] = "$canonicalName authentication successful";
       
   337                     if ($requireRebind === true) {
       
   338 	                    // rebinding with authenticated user
       
   339 	                    $ldap->bind($dn, $password);
       
   340                     }
       
   341                     return new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $canonicalName, $messages);
       
   342                 } else {
       
   343                     $messages[0] = 'Account is not a member of the specified group';
       
   344                     $messages[1] = $groupResult;
       
   345                     $failedAuthorities[$dname] = $groupResult;
       
   346                 }
       
   347             } catch (Zend_Ldap_Exception $zle) {
       
   348 
       
   349                 /* LDAP based authentication is notoriously difficult to diagnose. Therefore
       
   350                  * we bend over backwards to capture and record every possible bit of
       
   351                  * information when something goes wrong.
       
   352                  */
       
   353 
       
   354                 $err = $zle->getCode();
       
   355 
       
   356                 if ($err == Zend_Ldap_Exception::LDAP_X_DOMAIN_MISMATCH) {
       
   357                     /* This error indicates that the domain supplied in the
       
   358                      * username did not match the domains in the server options
       
   359                      * and therefore we should just skip to the next set of
       
   360                      * server options.
       
   361                      */
       
   362                     continue;
       
   363                 } else if ($err == Zend_Ldap_Exception::LDAP_NO_SUCH_OBJECT) {
       
   364                     $code = Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND;
       
   365                     $messages[0] = "Account not found: $username";
       
   366                     $failedAuthorities[$dname] = $zle->getMessage();
       
   367                 } else if ($err == Zend_Ldap_Exception::LDAP_INVALID_CREDENTIALS) {
       
   368                     $code = Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID;
       
   369                     $messages[0] = 'Invalid credentials';
       
   370                     $failedAuthorities[$dname] = $zle->getMessage();
       
   371                 } else {
       
   372                     $line = $zle->getLine();
       
   373                     $messages[] = $zle->getFile() . "($line): " . $zle->getMessage();
       
   374                     $messages[] = str_replace($password, '*****', $zle->getTraceAsString());
       
   375                     $messages[0] = 'An unexpected failure occurred';
       
   376                 }
       
   377                 $messages[1] = $zle->getMessage();
       
   378             }
       
   379         }
       
   380 
       
   381         $msg = isset($messages[1]) ? $messages[1] : $messages[0];
       
   382         $messages[] = "$username authentication failed: $msg";
       
   383 
       
   384         return new Zend_Auth_Result($code, $username, $messages);
       
   385     }
       
   386 
       
   387     /**
       
   388      * Sets the LDAP specific options on the Zend_Ldap instance
       
   389      *
       
   390      * @param  Zend_Ldap $ldap
       
   391      * @param  array $options
       
   392      * @return array of auth-adapter specific options
       
   393      */
       
   394     protected function _prepareOptions(Zend_Ldap $ldap, array $options)
       
   395     {
       
   396         $adapterOptions = array(
       
   397             'group'       => null,
       
   398             'groupDn'     => $ldap->getBaseDn(),
       
   399             'groupScope'  => Zend_Ldap::SEARCH_SCOPE_SUB,
       
   400             'groupAttr'   => 'cn',
       
   401             'groupFilter' => 'objectClass=groupOfUniqueNames',
       
   402             'memberAttr'  => 'uniqueMember',
       
   403             'memberIsDn'  => true
       
   404         );
       
   405         foreach ($adapterOptions as $key => $value) {
       
   406             if (array_key_exists($key, $options)) {
       
   407                 $value = $options[$key];
       
   408                 unset($options[$key]);
       
   409                 switch ($key) {
       
   410                     case 'groupScope':
       
   411                         $value = (int)$value;
       
   412                         if (in_array($value, array(Zend_Ldap::SEARCH_SCOPE_BASE,
       
   413                                 Zend_Ldap::SEARCH_SCOPE_ONE, Zend_Ldap::SEARCH_SCOPE_SUB), true)) {
       
   414                            $adapterOptions[$key] = $value;
       
   415                         }
       
   416                         break;
       
   417                     case 'memberIsDn':
       
   418                         $adapterOptions[$key] = ($value === true ||
       
   419                                 $value === '1' || strcasecmp($value, 'true') == 0);
       
   420                         break;
       
   421                     default:
       
   422                         $adapterOptions[$key] = trim($value);
       
   423                         break;
       
   424                 }
       
   425             }
       
   426         }
       
   427         $ldap->setOptions($options);
       
   428         return $adapterOptions;
       
   429     }
       
   430 
       
   431     /**
       
   432      * Checks the group membership of the bound user
       
   433      *
       
   434      * @param  Zend_Ldap $ldap
       
   435      * @param  string    $canonicalName
       
   436      * @param  string    $dn
       
   437      * @param  array     $adapterOptions
       
   438      * @return string|true
       
   439      */
       
   440     protected function _checkGroupMembership(Zend_Ldap $ldap, $canonicalName, $dn, array $adapterOptions)
       
   441     {
       
   442         if ($adapterOptions['group'] === null) {
       
   443             return true;
       
   444         }
       
   445 
       
   446         if ($adapterOptions['memberIsDn'] === false) {
       
   447             $user = $canonicalName;
       
   448         } else {
       
   449             $user = $dn;
       
   450         }
       
   451 
       
   452         /**
       
   453          * @see Zend_Ldap_Filter
       
   454          */
       
   455         require_once 'Zend/Ldap/Filter.php';
       
   456         $groupName = Zend_Ldap_Filter::equals($adapterOptions['groupAttr'], $adapterOptions['group']);
       
   457         $membership = Zend_Ldap_Filter::equals($adapterOptions['memberAttr'], $user);
       
   458         $group = Zend_Ldap_Filter::andFilter($groupName, $membership);
       
   459         $groupFilter = $adapterOptions['groupFilter'];
       
   460         if (!empty($groupFilter)) {
       
   461             $group = $group->addAnd($groupFilter);
       
   462         }
       
   463 
       
   464         $result = $ldap->count($group, $adapterOptions['groupDn'], $adapterOptions['groupScope']);
       
   465 
       
   466         if ($result === 1) {
       
   467             return true;
       
   468         } else {
       
   469             return 'Failed to verify group membership with ' . $group->toString();
       
   470         }
       
   471     }
       
   472 
       
   473     /**
       
   474      * getAccountObject() - Returns the result entry as a stdClass object
       
   475      *
       
   476      * This resembles the feature {@see Zend_Auth_Adapter_DbTable::getResultRowObject()}.
       
   477      * Closes ZF-6813
       
   478      *
       
   479      * @param  array $returnAttribs
       
   480      * @param  array $omitAttribs
       
   481      * @return stdClass|boolean
       
   482      */
       
   483     public function getAccountObject(array $returnAttribs = array(), array $omitAttribs = array())
       
   484     {
       
   485         if (!$this->_authenticatedDn) {
       
   486             return false;
       
   487         }
       
   488 
       
   489         $returnObject = new stdClass();
       
   490 
       
   491         $omitAttribs = array_map('strtolower', $omitAttribs);
       
   492 
       
   493         $entry = $this->getLdap()->getEntry($this->_authenticatedDn, $returnAttribs, true);
       
   494         foreach ($entry as $attr => $value) {
       
   495             if (in_array($attr, $omitAttribs)) {
       
   496                 // skip attributes marked to be omitted
       
   497                 continue;
       
   498             }
       
   499             if (is_array($value)) {
       
   500                 $returnObject->$attr = (count($value) > 1) ? $value : $value[0];
       
   501             } else {
       
   502                 $returnObject->$attr = $value;
       
   503             }
       
   504         }
       
   505         return $returnObject;
       
   506     }
       
   507 
       
   508     /**
       
   509      * Converts options to string
       
   510      *
       
   511      * @param  array $options
       
   512      * @return string
       
   513      */
       
   514     private function _optionsToString(array $options)
       
   515     {
       
   516         $str = '';
       
   517         foreach ($options as $key => $val) {
       
   518             if ($key === 'password')
       
   519                 $val = '*****';
       
   520             if ($str)
       
   521                 $str .= ',';
       
   522             $str .= $key . '=' . $val;
       
   523         }
       
   524         return $str;
       
   525     }
       
   526 }