1 | <?php
|
---|
2 | /**
|
---|
3 | * LDAP authentication backend
|
---|
4 | *
|
---|
5 | * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
|
---|
6 | * @author Andreas Gohr <[email protected]>
|
---|
7 | * @author Chris Smith <[email protected]>
|
---|
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 auth_ldap(){
|
---|
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 <[email protected]>
|
---|
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 <[email protected]>
|
---|
138 | * @author Trouble
|
---|
139 | * @author Dan Allen <[email protected]>
|
---|
140 | * @author <[email protected]>
|
---|
141 | * @author Stephane Chazelas <[email protected]>
|
---|
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 <[email protected]>
|
---|
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 |
|
---|
314 | /**
|
---|
315 | * Make LDAP filter strings.
|
---|
316 | *
|
---|
317 | * Used by auth_getUserData to make the filter
|
---|
318 | * strings for grouptree and groupfilter
|
---|
319 | *
|
---|
320 | * filter string ldap search filter with placeholders
|
---|
321 | * placeholders array array with the placeholders
|
---|
322 | *
|
---|
323 | * @author Troels Liebe Bentsen <[email protected]>
|
---|
324 | * @return string
|
---|
325 | */
|
---|
326 | function _makeFilter($filter, $placeholders) {
|
---|
327 | preg_match_all("/%{([^}]+)/", $filter, $matches, PREG_PATTERN_ORDER);
|
---|
328 | //replace each match
|
---|
329 | foreach ($matches[1] as $match) {
|
---|
330 | //take first element if array
|
---|
331 | if(is_array($placeholders[$match])) {
|
---|
332 | $value = $placeholders[$match][0];
|
---|
333 | } else {
|
---|
334 | $value = $placeholders[$match];
|
---|
335 | }
|
---|
336 | $value = $this->_filterEscape($value);
|
---|
337 | $filter = str_replace('%{'.$match.'}', $value, $filter);
|
---|
338 | }
|
---|
339 | return $filter;
|
---|
340 | }
|
---|
341 |
|
---|
342 | /**
|
---|
343 | * return 1 if $user + $info match $filter criteria, 0 otherwise
|
---|
344 | *
|
---|
345 | * @author Chris Smith <[email protected]>
|
---|
346 | */
|
---|
347 | function _filter($user, $info) {
|
---|
348 | foreach ($this->_pattern as $item => $pattern) {
|
---|
349 | if ($item == 'user') {
|
---|
350 | if (!preg_match($pattern, $user)) return 0;
|
---|
351 | } else if ($item == 'grps') {
|
---|
352 | if (!count(preg_grep($pattern, $info['grps']))) return 0;
|
---|
353 | } else {
|
---|
354 | if (!preg_match($pattern, $info[$item])) return 0;
|
---|
355 | }
|
---|
356 | }
|
---|
357 | return 1;
|
---|
358 | }
|
---|
359 |
|
---|
360 | function _constructPattern($filter) {
|
---|
361 | $this->_pattern = array();
|
---|
362 | foreach ($filter as $item => $pattern) {
|
---|
363 | // $this->_pattern[$item] = '/'.preg_quote($pattern,"/").'/i'; // don't allow regex characters
|
---|
364 | $this->_pattern[$item] = '/'.str_replace('/','\/',$pattern).'/i'; // allow regex characters
|
---|
365 | }
|
---|
366 | }
|
---|
367 |
|
---|
368 | /**
|
---|
369 | * Escape a string to be used in a LDAP filter
|
---|
370 | *
|
---|
371 | * Ported from Perl's Net::LDAP::Util escape_filter_value
|
---|
372 | *
|
---|
373 | * @author Andreas Gohr
|
---|
374 | */
|
---|
375 | function _filterEscape($string){
|
---|
376 | return preg_replace('/([\x00-\x1F\*\(\)\\\\])/e',
|
---|
377 | '"\\\\\".join("",unpack("H2","$1"))',
|
---|
378 | $string);
|
---|
379 | }
|
---|
380 |
|
---|
381 | /**
|
---|
382 | * Opens a connection to the configured LDAP server and sets the wanted
|
---|
383 | * option on the connection
|
---|
384 | *
|
---|
385 | * @author Andreas Gohr <[email protected]>
|
---|
386 | */
|
---|
387 | function _openLDAP(){
|
---|
388 | if($this->con) return true; // connection already established
|
---|
389 |
|
---|
390 | $this->bound = 0;
|
---|
391 |
|
---|
392 | $port = ($this->cnf['port']) ? $this->cnf['port'] : 389;
|
---|
393 | $this->con = @ldap_connect($this->cnf['server'],$port);
|
---|
394 | if(!$this->con){
|
---|
395 | msg("LDAP: couldn't connect to LDAP server",-1);
|
---|
396 | return false;
|
---|
397 | }
|
---|
398 |
|
---|
399 | //set protocol version and dependend options
|
---|
400 | if($this->cnf['version']){
|
---|
401 | if(!@ldap_set_option($this->con, LDAP_OPT_PROTOCOL_VERSION,
|
---|
402 | $this->cnf['version'])){
|
---|
403 | msg('Setting LDAP Protocol version '.$this->cnf['version'].' failed',-1);
|
---|
404 | if($this->cnf['debug'])
|
---|
405 | msg('LDAP version set: '.htmlspecialchars(ldap_error($this->con)),0,__LINE__,__FILE__);
|
---|
406 | }else{
|
---|
407 | //use TLS (needs version 3)
|
---|
408 | if($this->cnf['starttls']) {
|
---|
409 | if (!@ldap_start_tls($this->con)){
|
---|
410 | msg('Starting TLS failed',-1);
|
---|
411 | if($this->cnf['debug'])
|
---|
412 | msg('LDAP TLS set: '.htmlspecialchars(ldap_error($this->con)),0,__LINE__,__FILE__);
|
---|
413 | }
|
---|
414 | }
|
---|
415 | // needs version 3
|
---|
416 | if(isset($this->cnf['referrals'])) {
|
---|
417 | if(!@ldap_set_option($this->con, LDAP_OPT_REFERRALS,
|
---|
418 | $this->cnf['referrals'])){
|
---|
419 | msg('Setting LDAP referrals to off failed',-1);
|
---|
420 | if($this->cnf['debug'])
|
---|
421 | msg('LDAP referal set: '.htmlspecialchars(ldap_error($this->con)),0,__LINE__,__FILE__);
|
---|
422 | }
|
---|
423 | }
|
---|
424 | }
|
---|
425 | }
|
---|
426 |
|
---|
427 | //set deref mode
|
---|
428 | if($this->cnf['deref']){
|
---|
429 | if(!@ldap_set_option($this->con, LDAP_OPT_DEREF, $this->cnf['deref'])){
|
---|
430 | msg('Setting LDAP Deref mode '.$this->cnf['deref'].' failed',-1);
|
---|
431 | if($this->cnf['debug'])
|
---|
432 | msg('LDAP deref set: '.htmlspecialchars(ldap_error($this->con)),0,__LINE__,__FILE__);
|
---|
433 | }
|
---|
434 | }
|
---|
435 |
|
---|
436 | $this->canDo['getUsers'] = true;
|
---|
437 | return true;
|
---|
438 | }
|
---|
439 |
|
---|
440 | /**
|
---|
441 | * Wraps around ldap_search, ldap_list or ldap_read depending on $scope
|
---|
442 | *
|
---|
443 | * @param $scope string - can be 'base', 'one' or 'sub'
|
---|
444 | * @author Andreas Gohr <[email protected]>
|
---|
445 | */
|
---|
446 | function _ldapsearch($link_identifier, $base_dn, $filter, $scope='sub', $attributes=null,
|
---|
447 | $attrsonly=0, $sizelimit=0, $timelimit=0, $deref=LDAP_DEREF_NEVER){
|
---|
448 | if(is_null($attributes)) $attributes = array();
|
---|
449 |
|
---|
450 | if($scope == 'base'){
|
---|
451 | return @ldap_read($link_identifier, $base_dn, $filter, $attributes,
|
---|
452 | $attrsonly, $sizelimit, $timelimit, $deref);
|
---|
453 | }elseif($scope == 'one'){
|
---|
454 | return @ldap_list($link_identifier, $base_dn, $filter, $attributes,
|
---|
455 | $attrsonly, $sizelimit, $timelimit, $deref);
|
---|
456 | }else{
|
---|
457 | return @ldap_search($link_identifier, $base_dn, $filter, $attributes,
|
---|
458 | $attrsonly, $sizelimit, $timelimit, $deref);
|
---|
459 | }
|
---|
460 | }
|
---|
461 | }
|
---|
462 |
|
---|
463 | //Setup VIM: ex: et ts=4 :
|
---|