[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/auth/ -> ldap.class.php (source)

   1  <?php
   2  /**
   3   * LDAP authentication backend
   4   *
   5   * @license   GPL 2 (http://www.gnu.org/licenses/gpl.html)
   6   * @author    Andreas Gohr <andi@splitbrain.org>
   7   * @author    Chris Smith <chris@jalakaic.co.uk>
   8   */
   9  
  10  class auth_ldap extends auth_basic {
  11      var $cnf = null;
  12      var $con = null;
  13      var $bound = 0; // 0: anonymous, 1: user, 2: superuser
  14  
  15      /**
  16       * Constructor
  17       */
  18      function __construct(){
  19          global $conf;
  20          $this->cnf = $conf['auth']['ldap'];
  21  
  22          // ldap extension is needed
  23          if(!function_exists('ldap_connect')) {
  24              if ($this->cnf['debug'])
  25                  msg("LDAP err: PHP LDAP extension not found.",-1,__LINE__,__FILE__);
  26              $this->success = false;
  27              return;
  28          }
  29  
  30          if(empty($this->cnf['groupkey']))   $this->cnf['groupkey']   = 'cn';
  31          if(empty($this->cnf['userscope']))  $this->cnf['userscope']  = 'sub';
  32          if(empty($this->cnf['groupscope'])) $this->cnf['groupscope'] = 'sub';
  33  
  34          // auth_ldap currently just handles authentication, so no
  35          // capabilities are set
  36      }
  37  
  38      /**
  39       * Check user+password
  40       *
  41       * Checks if the given user exists and the given
  42       * plaintext password is correct by trying to bind
  43       * to the LDAP server
  44       *
  45       * @author  Andreas Gohr <andi@splitbrain.org>
  46       * @return  bool
  47       */
  48      function checkPass($user,$pass){
  49          // reject empty password
  50          if(empty($pass)) return false;
  51          if(!$this->_openLDAP()) return false;
  52  
  53          // indirect user bind
  54          if($this->cnf['binddn'] && $this->cnf['bindpw']){
  55              // use superuser credentials
  56              if(!@ldap_bind($this->con,$this->cnf['binddn'],$this->cnf['bindpw'])){
  57                  if($this->cnf['debug'])
  58                      msg('LDAP bind as superuser: '.htmlspecialchars(ldap_error($this->con)),0,__LINE__,__FILE__);
  59                  return false;
  60              }
  61              $this->bound = 2;
  62          }else if($this->cnf['binddn'] &&
  63                   $this->cnf['usertree'] &&
  64                   $this->cnf['userfilter']) {
  65              // special bind string
  66              $dn = $this->_makeFilter($this->cnf['binddn'],
  67                                       array('user'=>$user,'server'=>$this->cnf['server']));
  68  
  69          }else if(strpos($this->cnf['usertree'], '%{user}')) {
  70              // direct user bind
  71              $dn = $this->_makeFilter($this->cnf['usertree'],
  72                                       array('user'=>$user,'server'=>$this->cnf['server']));
  73  
  74          }else{
  75              // Anonymous bind
  76              if(!@ldap_bind($this->con)){
  77                  msg("LDAP: can not bind anonymously",-1);
  78                  if($this->cnf['debug'])
  79                      msg('LDAP anonymous bind: '.htmlspecialchars(ldap_error($this->con)),0,__LINE__,__FILE__);
  80                  return false;
  81              }
  82          }
  83  
  84          // Try to bind to with the dn if we have one.
  85          if(!empty($dn)) {
  86              // User/Password bind
  87              if(!@ldap_bind($this->con,$dn,$pass)){
  88                  if($this->cnf['debug']){
  89                      msg("LDAP: bind with $dn failed", -1,__LINE__,__FILE__);
  90                      msg('LDAP user dn bind: '.htmlspecialchars(ldap_error($this->con)),0);
  91                  }
  92                  return false;
  93              }
  94              $this->bound = 1;
  95              return true;
  96          }else{
  97              // See if we can find the user
  98              $info = $this->getUserData($user,true);
  99              if(empty($info['dn'])) {
 100                  return false;
 101              } else {
 102                  $dn = $info['dn'];
 103              }
 104  
 105              // Try to bind with the dn provided
 106              if(!@ldap_bind($this->con,$dn,$pass)){
 107                  if($this->cnf['debug']){
 108                      msg("LDAP: bind with $dn failed", -1,__LINE__,__FILE__);
 109                      msg('LDAP user bind: '.htmlspecialchars(ldap_error($this->con)),0);
 110                  }
 111                  return false;
 112              }
 113              $this->bound = 1;
 114              return true;
 115          }
 116  
 117          return false;
 118      }
 119  
 120      /**
 121       * Return user info
 122       *
 123       * Returns info about the given user needs to contain
 124       * at least these fields:
 125       *
 126       * name string  full name of the user
 127       * mail string  email addres of the user
 128       * grps array   list of groups the user is in
 129       *
 130       * This LDAP specific function returns the following
 131       * addional fields:
 132       *
 133       * dn     string  distinguished name (DN)
 134       * uid    string  Posix User ID
 135       * inbind bool    for internal use - avoid loop in binding
 136       *
 137       * @author  Andreas Gohr <andi@splitbrain.org>
 138       * @author  Trouble
 139       * @author  Dan Allen <dan.j.allen@gmail.com>
 140       * @author  <evaldas.auryla@pheur.org>
 141       * @author  Stephane Chazelas <stephane.chazelas@emerson.com>
 142       * @return  array containing user data or false
 143       */
 144      function getUserData($user,$inbind=false) {
 145          global $conf;
 146          if(!$this->_openLDAP()) return false;
 147  
 148          // force superuser bind if wanted and not bound as superuser yet
 149          if($this->cnf['binddn'] && $this->cnf['bindpw'] && $this->bound < 2){
 150              // use superuser credentials
 151              if(!@ldap_bind($this->con,$this->cnf['binddn'],$this->cnf['bindpw'])){
 152                  if($this->cnf['debug'])
 153                      msg('LDAP bind as superuser: '.htmlspecialchars(ldap_error($this->con)),0,__LINE__,__FILE__);
 154                  return false;
 155              }
 156              $this->bound = 2;
 157          }elseif($this->bound == 0 && !$inbind) {
 158              // in some cases getUserData is called outside the authentication workflow
 159              // eg. for sending email notification on subscribed pages. This data might not
 160              // be accessible anonymously, so we try to rebind the current user here
 161              list($loginuser,$loginsticky,$loginpass) = auth_getCookie();
 162              if($loginuser && $loginpass){
 163                  $loginpass = PMA_blowfish_decrypt($loginpass, auth_cookiesalt(!$loginsticky));
 164                  $this->checkPass($loginuser, $loginpass);
 165              }
 166          }
 167  
 168          $info['user']   = $user;
 169          $info['server'] = $this->cnf['server'];
 170  
 171          //get info for given user
 172          $base = $this->_makeFilter($this->cnf['usertree'], $info);
 173          if(!empty($this->cnf['userfilter'])) {
 174              $filter = $this->_makeFilter($this->cnf['userfilter'], $info);
 175          } else {
 176              $filter = "(ObjectClass=*)";
 177          }
 178  
 179          $sr     = $this->_ldapsearch($this->con, $base, $filter, $this->cnf['userscope']);
 180          $result = @ldap_get_entries($this->con, $sr);
 181          if($this->cnf['debug']){
 182              msg('LDAP user search: '.htmlspecialchars(ldap_error($this->con)),0,__LINE__,__FILE__);
 183              msg('LDAP search at: '.htmlspecialchars($base.' '.$filter),0,__LINE__,__FILE__);
 184          }
 185  
 186          // Don't accept more or less than one response
 187          if(!is_array($result) || $result['count'] != 1){
 188              return false; //user not found
 189          }
 190  
 191          $user_result = $result[0];
 192          ldap_free_result($sr);
 193  
 194          // general user info
 195          $info['dn']   = $user_result['dn'];
 196          $info['gid']  = $user_result['gidnumber'][0];
 197          $info['mail'] = $user_result['mail'][0];
 198          $info['name'] = $user_result['cn'][0];
 199          $info['grps'] = array();
 200  
 201          // overwrite if other attribs are specified.
 202          if(is_array($this->cnf['mapping'])){
 203              foreach($this->cnf['mapping'] as $localkey => $key) {
 204                  if(is_array($key)) {
 205                      // use regexp to clean up user_result
 206                      list($key, $regexp) = each($key);
 207                      if($user_result[$key]) foreach($user_result[$key] as $grp){
 208                          if (preg_match($regexp,$grp,$match)) {
 209                              if($localkey == 'grps') {
 210                                  $info[$localkey][] = $match[1];
 211                              } else {
 212                                  $info[$localkey] = $match[1];
 213                              }
 214                          }
 215                      }
 216                  } else {
 217                      $info[$localkey] = $user_result[$key][0];
 218                  }
 219              }
 220          }
 221          $user_result = array_merge($info,$user_result);
 222  
 223          //get groups for given user if grouptree is given
 224          if ($this->cnf['grouptree'] || $this->cnf['groupfilter']) {
 225              $base   = $this->_makeFilter($this->cnf['grouptree'], $user_result);
 226              $filter = $this->_makeFilter($this->cnf['groupfilter'], $user_result);
 227              $sr = $this->_ldapsearch($this->con, $base, $filter, $this->cnf['groupscope'], array($this->cnf['groupkey']));
 228              if($this->cnf['debug']){
 229                  msg('LDAP group search: '.htmlspecialchars(ldap_error($this->con)),0,__LINE__,__FILE__);
 230                  msg('LDAP search at: '.htmlspecialchars($base.' '.$filter),0,__LINE__,__FILE__);
 231              }
 232              if(!$sr){
 233                  msg("LDAP: Reading group memberships failed",-1);
 234                  return false;
 235              }
 236              $result = ldap_get_entries($this->con, $sr);
 237              ldap_free_result($sr);
 238  
 239              if(is_array($result)) foreach($result as $grp){
 240                  if(!empty($grp[$this->cnf['groupkey']][0])){
 241                      if($this->cnf['debug'])
 242                          msg('LDAP usergroup: '.htmlspecialchars($grp[$this->cnf['groupkey']][0]),0,__LINE__,__FILE__);
 243                      $info['grps'][] = $grp[$this->cnf['groupkey']][0];
 244                  }
 245              }
 246          }
 247  
 248          // always add the default group to the list of groups
 249          if(!in_array($conf['defaultgroup'],$info['grps'])){
 250              $info['grps'][] = $conf['defaultgroup'];
 251          }
 252          return $info;
 253      }
 254  
 255      /**
 256       * Most values in LDAP are case-insensitive
 257       */
 258      function isCaseSensitive(){
 259          return false;
 260      }
 261  
 262      /**
 263       * Bulk retrieval of user data
 264       *
 265       * @author  Dominik Eckelmann <dokuwiki@cosmocode.de>
 266       * @param   start     index of first user to be returned
 267       * @param   limit     max number of users to be returned
 268       * @param   filter    array of field/pattern pairs, null for no filter
 269       * @return  array of userinfo (refer getUserData for internal userinfo details)
 270       */
 271      function retrieveUsers($start=0,$limit=-1,$filter=array()) {
 272          if(!$this->_openLDAP()) return false;
 273  
 274          if (!isset($this->users)) {
 275              // Perform the search and grab all their details
 276              if(!empty($this->cnf['userfilter'])) {
 277                  $all_filter = str_replace('%{user}', '*', $this->cnf['userfilter']);
 278              } else {
 279                  $all_filter = "(ObjectClass=*)";
 280              }
 281              $sr=ldap_search($this->con,$this->cnf['usertree'],$all_filter);
 282              $entries = ldap_get_entries($this->con, $sr);
 283              $users_array = array();
 284              for ($i=0; $i<$entries["count"]; $i++){
 285                  array_push($users_array, $entries[$i]["uid"][0]);
 286              }
 287              asort($users_array);
 288              $result = $users_array;
 289              if (!$result) return array();
 290              $this->users = array_fill_keys($result, false);
 291          }
 292          $i = 0;
 293          $count = 0;
 294          $this->_constructPattern($filter);
 295          $result = array();
 296  
 297          foreach ($this->users as $user => &$info) {
 298              if ($i++ < $start) {
 299                  continue;
 300              }
 301              if ($info === false) {
 302                  $info = $this->getUserData($user);
 303              }
 304              if ($this->_filter($user, $info)) {
 305                  $result[$user] = $info;
 306                  if (($limit >= 0) && (++$count >= $limit)) break;
 307              }
 308          }
 309          return $result;
 310      }
 311  
 312      /**
 313       * Make LDAP filter strings.
 314       *
 315       * Used by auth_getUserData to make the filter
 316       * strings for grouptree and groupfilter
 317       *
 318       * filter      string  ldap search filter with placeholders
 319       * placeholders array   array with the placeholders
 320       *
 321       * @author  Troels Liebe Bentsen <tlb@rapanden.dk>
 322       * @return  string
 323       */
 324      function _makeFilter($filter, $placeholders) {
 325          preg_match_all("/%{([^}]+)/", $filter, $matches, PREG_PATTERN_ORDER);
 326          //replace each match
 327          foreach ($matches[1] as $match) {
 328              //take first element if array
 329              if(is_array($placeholders[$match])) {
 330                  $value = $placeholders[$match][0];
 331              } else {
 332                  $value = $placeholders[$match];
 333              }
 334              $value = $this->_filterEscape($value);
 335              $filter = str_replace('%{'.$match.'}', $value, $filter);
 336          }
 337          return $filter;
 338      }
 339  
 340      /**
 341       * return 1 if $user + $info match $filter criteria, 0 otherwise
 342       *
 343       * @author   Chris Smith <chris@jalakai.co.uk>
 344       */
 345      function _filter($user, $info) {
 346          foreach ($this->_pattern as $item => $pattern) {
 347              if ($item == 'user') {
 348                  if (!preg_match($pattern, $user)) return 0;
 349              } else if ($item == 'grps') {
 350                  if (!count(preg_grep($pattern, $info['grps']))) return 0;
 351              } else {
 352                  if (!preg_match($pattern, $info[$item])) return 0;
 353              }
 354          }
 355          return 1;
 356      }
 357  
 358      function _constructPattern($filter) {
 359          $this->_pattern = array();
 360          foreach ($filter as $item => $pattern) {
 361              $this->_pattern[$item] = '/'.str_replace('/','\/',$pattern).'/i';    // allow regex characters
 362          }
 363      }
 364  
 365      /**
 366       * Escape a string to be used in a LDAP filter
 367       *
 368       * Ported from Perl's Net::LDAP::Util escape_filter_value
 369       *
 370       * @author Andreas Gohr
 371       */
 372      function _filterEscape($string){
 373          return preg_replace('/([\x00-\x1F\*\(\)\\\\])/e',
 374                              '"\\\\\".join("",unpack("H2","$1"))',
 375                              $string);
 376      }
 377  
 378      /**
 379       * Opens a connection to the configured LDAP server and sets the wanted
 380       * option on the connection
 381       *
 382       * @author  Andreas Gohr <andi@splitbrain.org>
 383       */
 384      function _openLDAP(){
 385          if($this->con) return true; // connection already established
 386  
 387          $this->bound = 0;
 388  
 389          $port = ($this->cnf['port']) ? $this->cnf['port'] : 389;
 390          $bound = false;
 391          $servers = explode(',', $this->cnf['server']);
 392          foreach ($servers as $server) {
 393              $server = trim($server);
 394              $this->con = @ldap_connect($server, $port);
 395              if (!$this->con) {
 396                  continue;
 397              }
 398  
 399              /*
 400               * When OpenLDAP 2.x.x is used, ldap_connect() will always return a resource as it does
 401               * not actually connect but just initializes the connecting parameters. The actual
 402               * connect happens with the next calls to ldap_* funcs, usually with ldap_bind().
 403               *
 404               * So we should try to bind to server in order to check its availability.
 405               */
 406  
 407              //set protocol version and dependend options
 408              if($this->cnf['version']){
 409                  if(!@ldap_set_option($this->con, LDAP_OPT_PROTOCOL_VERSION,
 410                                       $this->cnf['version'])){
 411                      msg('Setting LDAP Protocol version '.$this->cnf['version'].' failed',-1);
 412                      if($this->cnf['debug'])
 413                          msg('LDAP version set: '.htmlspecialchars(ldap_error($this->con)),0,__LINE__,__FILE__);
 414                  }else{
 415                      //use TLS (needs version 3)
 416                      if($this->cnf['starttls']) {
 417                          if (!@ldap_start_tls($this->con)){
 418                              msg('Starting TLS failed',-1);
 419                              if($this->cnf['debug'])
 420                                  msg('LDAP TLS set: '.htmlspecialchars(ldap_error($this->con)),0,__LINE__,__FILE__);
 421                          }
 422                      }
 423                      // needs version 3
 424                      if(isset($this->cnf['referrals'])) {
 425                          if(!@ldap_set_option($this->con, LDAP_OPT_REFERRALS,
 426                             $this->cnf['referrals'])){
 427                              msg('Setting LDAP referrals to off failed',-1);
 428                              if($this->cnf['debug'])
 429                                  msg('LDAP referal set: '.htmlspecialchars(ldap_error($this->con)),0,__LINE__,__FILE__);
 430                          }
 431                      }
 432                  }
 433              }
 434  
 435              //set deref mode
 436              if($this->cnf['deref']){
 437                  if(!@ldap_set_option($this->con, LDAP_OPT_DEREF, $this->cnf['deref'])){
 438                      msg('Setting LDAP Deref mode '.$this->cnf['deref'].' failed',-1);
 439                      if($this->cnf['debug'])
 440                          msg('LDAP deref set: '.htmlspecialchars(ldap_error($this->con)),0,__LINE__,__FILE__);
 441                  }
 442              }
 443              /* As of PHP 5.3.0 we can set timeout to speedup skipping of invalid servers */
 444              if (defined('LDAP_OPT_NETWORK_TIMEOUT')) {
 445                  ldap_set_option($this->con, LDAP_OPT_NETWORK_TIMEOUT, 1);
 446              }
 447              $bound = @ldap_bind($this->con);
 448              if ($bound) {
 449                  break;
 450              }
 451          }
 452  
 453          if(!$bound) {
 454              msg("LDAP: couldn't connect to LDAP server",-1);
 455              return false;
 456          }
 457  
 458  
 459          $this->canDo['getUsers'] = true;
 460          return true;
 461      }
 462  
 463      /**
 464       * Wraps around ldap_search, ldap_list or ldap_read depending on $scope
 465       *
 466       * @param  $scope string - can be 'base', 'one' or 'sub'
 467       * @author Andreas Gohr <andi@splitbrain.org>
 468       */
 469      function _ldapsearch($link_identifier, $base_dn, $filter, $scope='sub', $attributes=null,
 470                           $attrsonly=0, $sizelimit=0, $timelimit=0, $deref=LDAP_DEREF_NEVER){
 471          if(is_null($attributes)) $attributes = array();
 472  
 473          if($scope == 'base'){
 474              return @ldap_read($link_identifier, $base_dn, $filter, $attributes,
 475                               $attrsonly, $sizelimit, $timelimit, $deref);
 476          }elseif($scope == 'one'){
 477              return @ldap_list($link_identifier, $base_dn, $filter, $attributes,
 478                               $attrsonly, $sizelimit, $timelimit, $deref);
 479          }else{
 480              return @ldap_search($link_identifier, $base_dn, $filter, $attributes,
 481                                  $attrsonly, $sizelimit, $timelimit, $deref);
 482          }
 483      }
 484  }
 485  
 486  //Setup VIM: ex: et ts=4 :


Generated: Sun Feb 3 03:00:06 2013 Cross-referenced by PHPXref 0.7
WikiForumIRCBugsGitXRefTranslate