df78de499c0d9cf8eb548528dfe29b1e5e16ccaa
[racktables] / wwwroot / inc / auth.php
1 <?php
2
3 # This file is a part of RackTables, a datacenter and server room management
4 # framework. See accompanying file "COPYING" for the full copyright and
5 # licensing information.
6
7 /*
8
9 Below is a mix of authentication (confirming user's identity) and authorization
10 (access controlling) functions of RackTables. The former set is expected to
11 be working with only database.php file included.
12
13 */
14
15 // This function ensures that we don't continue without a legitimate
16 // username and password (also make sure, that both are present, this
17 // is especially useful for LDAP auth code to not deceive itself with
18 // anonymous binding). It also initializes $remote_* and $*_tags vars.
19 function authenticate ()
20 {
21 function assertHTTPCredentialsReceived()
22 {
23 if
24 (
25 ! isset ($_SERVER['PHP_AUTH_USER']) ||
26 $_SERVER['PHP_AUTH_USER'] == '' ||
27 ! isset ($_SERVER['PHP_AUTH_PW']) ||
28 $_SERVER['PHP_AUTH_PW'] == ''
29 )
30 throw new RackTablesError ('', RackTablesError::NOT_AUTHENTICATED);
31 }
32
33 global
34 $remote_username,
35 $remote_displayname,
36 $auto_tags,
37 $user_given_tags,
38 $user_auth_src,
39 $script_mode,
40 $require_local_account;
41 // Phase 1. Assert basic pre-requisites, short-circuit the logout request.
42 if (! isset ($user_auth_src) || ! isset ($require_local_account))
43 throw new RackTablesError ('secret.php: either user_auth_src or require_local_account are missing', RackTablesError::MISCONFIGURED);
44 if (isset ($_REQUEST['logout']))
45 {
46 if (isset ($user_auth_src) && 'saml' == $user_auth_src)
47 saml_logout ();
48 throw new RackTablesError ('', RackTablesError::NOT_AUTHENTICATED); // Reset browser credentials cache.
49 }
50 // Phase 2. Do some method-specific processing, initialize $remote_username on success.
51 switch (TRUE)
52 {
53 case isset ($script_mode) && $script_mode && isset ($remote_username) && $remote_username != '':
54 break; // skip this phase
55 case 'database' == $user_auth_src:
56 assertHTTPCredentialsReceived();
57 $remote_username = $_SERVER['PHP_AUTH_USER'];
58 break;
59 case 'ldap' == $user_auth_src:
60 assertHTTPCredentialsReceived();
61 $remote_username = $_SERVER['PHP_AUTH_USER'];
62 constructLDAPOptions();
63 break;
64 case 'httpd' == $user_auth_src:
65 if
66 (
67 ! isset ($_SERVER['REMOTE_USER']) or
68 $_SERVER['REMOTE_USER'] == ''
69 )
70 throw new RackTablesError ('The web-server didn\'t authenticate the user, although ought to do.', RackTablesError::MISCONFIGURED);
71 $remote_username = $_SERVER['REMOTE_USER'];
72 break;
73 case 'saml' == $user_auth_src:
74 $saml_username = '';
75 $saml_dispname = '';
76 if (! authenticated_via_saml ($saml_username, $saml_dispname))
77 throw new RackTablesError ('', RackTablesError::NOT_AUTHENTICATED);
78 $remote_username = $saml_username;
79 break;
80 default:
81 throw new RackTablesError ('Invalid authentication source!', RackTablesError::MISCONFIGURED);
82 }
83 // Phase 3. Handle local account requirement, pull user tags into security context.
84 $userinfo = constructUserCell ($remote_username);
85 if ($require_local_account && ! isset ($userinfo['user_id']))
86 throw new RackTablesError ('', RackTablesError::NOT_AUTHENTICATED);
87 $user_given_tags = $userinfo['etags'];
88 $auto_tags = array_merge ($auto_tags, $userinfo['atags']);
89 // Phase 4. Do more method-specific processing, initialize $remote_displayname on success.
90 switch (TRUE)
91 {
92 case isset ($script_mode) && $script_mode:
93 return; // success
94 // Just trust the server, because the password isn't known.
95 case 'httpd' == $user_auth_src:
96 $remote_displayname = $userinfo['user_realname'] != '' ?
97 $userinfo['user_realname'] :
98 $remote_username;
99 return; // success
100 // When using LDAP, leave a mean to fix things. Admin user is always authenticated locally.
101 case array_key_exists ('user_id', $userinfo) && $userinfo['user_id'] == 1:
102 case 'database' == $user_auth_src:
103 $remote_displayname = $userinfo['user_realname'] != '' ?
104 $userinfo['user_realname'] :
105 $remote_username;
106 if (authenticated_via_database ($userinfo, $_SERVER['PHP_AUTH_PW']))
107 return; // success
108 break; // failure
109 case 'ldap' == $user_auth_src:
110 $ldap_dispname = '';
111 if (! authenticated_via_ldap ($remote_username, $_SERVER['PHP_AUTH_PW'], $ldap_dispname))
112 break; // failure
113 $remote_displayname = $userinfo['user_realname'] != '' ? // local value is most preferred
114 $userinfo['user_realname'] :
115 ($ldap_dispname != '' ? $ldap_dispname : $remote_username); // then one from LDAP
116 return; // success
117 case 'saml' == $user_auth_src:
118 $remote_displayname = $saml_dispname != '' ? $saml_dispname : $saml_username;
119 return; // success
120 default:
121 throw new RackTablesError ('Invalid authentication source!', RackTablesError::MISCONFIGURED);
122 }
123 throw new RackTablesError ('', RackTablesError::NOT_AUTHENTICATED);
124 }
125
126 // Merge accumulated tags into a single chain, add location-specific
127 // autotags and try getting access clearance. Page and tab are mandatory,
128 // operation is optional.
129 function permitted ($p = NULL, $t = NULL, $o = NULL, $annex = array())
130 {
131 global $pageno, $tabno, $op;
132 global $auto_tags;
133
134 if ($p === NULL)
135 $p = $pageno;
136 if ($t === NULL)
137 $t = $tabno;
138 if ($o === NULL && $op != '') // $op can be set to empty string
139 $o = $op;
140 $my_auto_tags = $auto_tags;
141 $my_auto_tags[] = array ('tag' => '$page_' . $p);
142 $my_auto_tags[] = array ('tag' => '$tab_' . $t);
143 if ($o !== NULL) // these tags only make sense in certain cases
144 {
145 $my_auto_tags[] = array ('tag' => '$op_' . $o);
146 $my_auto_tags[] = array ('tag' => '$any_op');
147 }
148 $subject = array_merge
149 (
150 $my_auto_tags,
151 $annex
152 );
153 // XXX: The solution below is only appropriate for a corner case of a more universal
154 // problem: to make the decision for an entity belonging to a cascade of nested
155 // containers. Each container being an entity itself, it may have own tags (explicit
156 // and implicit accordingly). There's a fixed set of rules (RackCode) with each rule
157 // being able to evaluate any built and given context and produce either a decision
158 // or a lack of decision.
159 // There are several levels of context for the target entity, at least one for entities
160 // belonging directly to the tree root. Each level's context is a union of given
161 // container's tags and the tags of the contained entities.
162 // The universal problem originates from the fact, that certain rules may change
163 // their product as context level changes, thus forcing some final decision (but not
164 // adding a lack of it). With rule code being principles and context cascade being
165 // circumstances, there are two uttermost approaches or moralities.
166 //
167 // Fundamentalism: principles over circumstances. When a rule doesn't produce any
168 // decision, go on to the next rule. When all rules are evaluated, go on to the next
169 // security context level.
170 //
171 // Opportunism: circumstances over principles. With a lack of decision, work with the
172 // same rule, trying to evaluate it against the next level (and next, and next...),
173 // until all levels are tried. Only then go on to the next rule.
174 //
175 // With the above being simple discrete algorythms, I believe, that they very reliably
176 // replicate human behavior. This gives a vast ground for further research, so I would
177 // only note, that the morale used in RackTables is "principles first".
178 return gotClearanceForTagChain ($subject);
179 }
180
181 # a "throwing" wrapper for above
182 function assertPermission ($p = NULL, $t = NULL, $o = NULL, $annex = array())
183 {
184 if (! permitted ($p, $t, $o, $annex))
185 throw new RTPermissionDenied();
186 }
187
188 # Process a (globally available) RackCode permissions parse tree (which
189 # stands for a sequence of rules), evaluating each rule against a list of
190 # tags. This list of tags consists of (globally available) explicit and
191 # implicit tags plus some extra tags, available through the argument of the
192 # function. The latter tags are referred to as "constant" tags, because
193 # RackCode syntax allows for "context modifier" constructs, which result in
194 # implicit and explicit tags being assigned or unassigned. Such context
195 # changes remain in effect even upon return from this function.
196 function gotClearanceForTagChain ($const_base)
197 {
198 global $rackCode, $expl_tags, $impl_tags;
199 $context = array_merge ($const_base, $expl_tags, $impl_tags);
200 $context = reindexById ($context, 'tag', TRUE);
201
202 foreach ($rackCode as $sentence)
203 {
204 switch ($sentence['type'])
205 {
206 case 'SYNT_GRANT':
207 if (eval_expression ($sentence['condition'], $context))
208 return $sentence['decision'];
209 break;
210 case 'SYNT_ADJUSTMENT':
211 if
212 (
213 eval_expression ($sentence['condition'], $context) &&
214 processAdjustmentSentence ($sentence['modlist'], $expl_tags)
215 ) // recalculate implicit chain only after actual change, not just on matched condition
216 {
217 $impl_tags = getImplicitTags ($expl_tags); // recalculate
218 $context = array_merge ($const_base, $expl_tags, $impl_tags);
219 $context = reindexById ($context, 'tag', TRUE);
220 }
221 break;
222 default:
223 throw new RackTablesError ("Can't process sentence of unknown type '${sentence['type']}'", RackTablesError::INTERNAL);
224 }
225 }
226 return FALSE;
227 }
228
229 // Process a context adjustment request, update given chain accordingly,
230 // return TRUE on any changes done.
231 // The request is a sequence of clear/insert/remove requests exactly as cooked
232 // for each SYNT_CTXMODLIST node.
233 function processAdjustmentSentence ($modlist, &$chain)
234 {
235 global $rackCode;
236 $didChanges = FALSE;
237 foreach ($modlist as $mod)
238 switch ($mod['op'])
239 {
240 case 'insert':
241 foreach ($chain as $etag)
242 if ($etag['tag'] == $mod['tag']) // already there, next request
243 break 2;
244 $search = getTagByName ($mod['tag']);
245 if ($search === NULL) // skip martians silently
246 break;
247 $chain[] = $search;
248 $didChanges = TRUE;
249 break;
250 case 'remove':
251 foreach ($chain as $key => $etag)
252 if ($etag['tag'] == $mod['tag']) // drop first match and return
253 {
254 unset ($chain[$key]);
255 $didChanges = TRUE;
256 break 2;
257 }
258 break;
259 case 'clear':
260 $chain = array();
261 $didChanges = TRUE;
262 break;
263 default: // HCF
264 throw new RackTablesError ('invalid structure', RackTablesError::INTERNAL);
265 }
266 return $didChanges;
267 }
268
269 // a wrapper for SAML auth method
270 function authenticated_via_saml (&$saml_username = NULL, &$saml_displayname = NULL)
271 {
272 global $SAML_options, $debug_mode, $auto_tags;
273 if (! file_exists ($SAML_options['simplesamlphp_basedir'] . '/lib/_autoload.php'))
274 throw new RackTablesError ('Configured for SAML authentication, but simplesaml is not found.', RackTablesError::MISCONFIGURED);
275 require_once ($SAML_options['simplesamlphp_basedir'] . '/lib/_autoload.php');
276 $as = new SimpleSAML_Auth_Simple ($SAML_options['sp_profile']);
277 if (! $as->isAuthenticated())
278 $as->requireAuth();
279 $attributes = $as->getAttributes();
280 $saml_username = saml_getAttributeValue ($attributes, $SAML_options['usernameAttribute']);
281 $saml_displayname = saml_getAttributeValue ($attributes, $SAML_options['fullnameAttribute']);
282 if (array_key_exists ('groupListAttribute', $SAML_options))
283 foreach (saml_getAttributeValues ($attributes, $SAML_options['groupListAttribute']) as $autotag)
284 $auto_tags[] = array ('tag' => '$sgcn_' . $autotag);
285 return $as->isAuthenticated();
286 }
287
288 function saml_logout ()
289 {
290 global $SAML_options;
291 if (! file_exists ($SAML_options['simplesamlphp_basedir'] . '/lib/_autoload.php'))
292 throw new RackTablesError ('Configured for SAML authentication, but simplesaml is not found.', RackTablesError::MISCONFIGURED);
293 require_once ($SAML_options['simplesamlphp_basedir'] . '/lib/_autoload.php');
294 $as = new SimpleSAML_Auth_Simple ($SAML_options['sp_profile']);
295 header("Location: ".$as->getLogoutURL('/'));
296 exit;
297 }
298
299 function saml_getAttributeValue ($attributes, $name)
300 {
301 if (! isset ($attributes[$name]))
302 return '';
303 return is_array ($attributes[$name]) ? $attributes[$name][0] : $attributes[$name];
304 }
305
306 function saml_getAttributeValues ($attributes, $name)
307 {
308 if (! isset ($attributes[$name]))
309 return array();
310 return is_array ($attributes[$name]) ? $attributes[$name] : array($attributes[$name]);
311 }
312
313 function constructLDAPOptions()
314 {
315 global $LDAP_options;
316 if (! isset ($LDAP_options))
317 throw new RackTablesError ('$LDAP_options has not been defined (see secret.php)', RackTablesError::MISCONFIGURED);
318 $LDAP_defaults = array
319 (
320 'group_attr' => 'memberof',
321 'group_filter' => '/^[Cc][Nn]=([^,]+)/',
322 'cache_refresh' => 300,
323 'cache_retry' => 15,
324 'cache_expiry' => 600,
325 );
326 foreach ($LDAP_defaults as $option_name => $option_value)
327 if (! array_key_exists ($option_name, $LDAP_options))
328 $LDAP_options[$option_name] = $option_value;
329 }
330
331 // a wrapper for two LDAP auth methods below
332 function authenticated_via_ldap ($username, $password, &$ldap_displayname)
333 {
334 global $LDAP_options, $debug_mode;
335 try
336 {
337 // Destroy the cache each time config changes.
338 if ($LDAP_options['cache_expiry'] != 0 &&
339 sha1 (serialize ($LDAP_options)) != loadScript ('LDAPConfigHash'))
340 {
341 discardLDAPCache();
342 saveScript ('LDAPConfigHash', sha1 (serialize ($LDAP_options)));
343 deleteScript ('LDAPLastSuccessfulServer');
344 }
345
346 if
347 (
348 $LDAP_options['cache_retry'] > $LDAP_options['cache_refresh'] ||
349 $LDAP_options['cache_refresh'] > $LDAP_options['cache_expiry']
350 )
351 throw new RackTablesError ('LDAP misconfiguration: refresh/retry/expiry mismatch', RackTablesError::MISCONFIGURED);
352 if ($LDAP_options['cache_expiry'] == 0) // immediate expiry set means disabled cache
353 return authenticated_via_ldap_nocache ($username, $password, $ldap_displayname);
354 // authenticated_via_ldap_cache()'s way of locking can sometimes result in
355 // a PDO error condition that convertPDOException() was not able to dispatch.
356 // To avoid reaching printPDOException() (which prints backtrace with password
357 // argument in cleartext), any remaining PDO condition is converted locally.
358 return authenticated_via_ldap_cache ($username, $password, $ldap_displayname);
359 }
360 catch (PDOException $e)
361 {
362 if (isset ($debug_mode) && $debug_mode)
363 // in debug mode re-throw DB exception as-is
364 throw $e;
365 else
366 // re-create exception to hide private data from its backtrace
367 throw new RackTablesError ('LDAP caching error', RackTablesError::DB_WRITE_FAILED);
368 }
369 }
370
371 // Authenticate given user with known LDAP server, completely ignore LDAP cache data.
372 function authenticated_via_ldap_nocache ($username, $password, &$ldap_displayname)
373 {
374 global $auto_tags;
375 $server_test = queryLDAPServer ($username, $password);
376 if ($server_test['result'] == 'ACK')
377 {
378 $ldap_displayname = $server_test['displayed_name'];
379 foreach ($server_test['memberof'] as $autotag)
380 $auto_tags[] = array ('tag' => $autotag);
381 return TRUE;
382 }
383 return FALSE;
384 }
385
386 // check that LDAP cache row contains correct password and is not expired
387 // if check_for_refreshing = TRUE, also checks that cache row does not need refreshing
388 function isLDAPCacheValid ($cache_row, $password_hash, $check_for_refreshing = FALSE)
389 {
390 global $LDAP_options;
391 return
392 is_array ($cache_row) &&
393 $cache_row['successful_hash'] === $password_hash &&
394 $cache_row['success_age'] < $LDAP_options['cache_expiry'] &&
395 (
396 // There are two confidence levels of cache hits: "certain" and "uncertain". In either case
397 // expect authentication success, unless it's well-timed to perform a retry,
398 // which may sometimes bring a NAK decision.
399 ! $check_for_refreshing ||
400 (
401 $cache_row['success_age'] < $LDAP_options['cache_refresh'] ||
402 isset ($cache_row['retry_age']) &&
403 $cache_row['retry_age'] < $LDAP_options['cache_retry']
404 )
405 );
406 }
407
408 // Idem, but consider existing data in cache and modify/discard it, when necessary.
409 // Remember to have releaseLDAPCache() called before any return statement.
410 // Perform cache maintenance on each update.
411 function authenticated_via_ldap_cache ($username, $password, &$ldap_displayname)
412 {
413 global $LDAP_options, $auto_tags;
414
415 $user_data = array(); // fill auto_tags and ldap_displayname from this array
416 $password_hash = sha1 ($password);
417
418 // first try to get cache row without locking it (quick way)
419 $cache_row = fetchLDAPCacheRow ($username);
420 if (isLDAPCacheValid ($cache_row, $password_hash, TRUE))
421 $user_data = $cache_row; // cache HIT
422 else
423 {
424 // cache miss or expired. Try to lock LDAPCache for $username
425 $cache_row = acquireLDAPCache ($username);
426 if (isLDAPCacheValid ($cache_row, $password_hash, TRUE))
427 $user_data = $cache_row; // cache HIT, but with DB lock
428 else
429 {
430 $ldap_answer = queryLDAPServer ($username, $password);
431 switch ($ldap_answer['result'])
432 {
433 case 'ACK':
434 replaceLDAPCacheRecord ($username, $password_hash, $ldap_answer['displayed_name'], $ldap_answer['memberof']);
435 $user_data = $ldap_answer;
436 break;
437 case 'NAK': // The record isn't valid any more.
438 // TODO: negative result caching
439 deleteLDAPCacheRecord ($username);
440 break;
441 case 'CAN': // LDAP query failed, use old value till next retry
442 if (isLDAPCacheValid ($cache_row, $password_hash, FALSE))
443 {
444 touchLDAPCacheRecord ($username);
445 $user_data = $cache_row;
446 }
447 else
448 deleteLDAPCacheRecord ($username);
449 break;
450 default:
451 throw new RackTablesError ('structure error', RackTablesError::INTERNAL);
452 }
453 }
454 releaseLDAPCache();
455 }
456
457 if ($user_data)
458 {
459 $ldap_displayname = $user_data['displayed_name'];
460 foreach ($user_data['memberof'] as $autotag)
461 $auto_tags[] = array ('tag' => $autotag);
462 return TRUE;
463 }
464 return FALSE;
465 }
466
467 // Attempt a server conversation and return an array describing the outcome:
468 //
469 // 'result' => 'CAN' : connect (or search) failed completely
470 //
471 // 'result' => 'NAK' : server replied and denied access (or search returned odd data)
472 //
473 // 'result' => 'ACK' : server replied and cleared access, there were no search errors
474 // 'displayed_name' : a string built according to LDAP displayname_attrs option
475 // 'memberof' => filtered list of all LDAP groups the user belongs to
476 //
477 function queryLDAPServer ($username, $password)
478 {
479 global $LDAP_options;
480
481 if (extension_loaded ('ldap') === FALSE)
482 throw new RackTablesError ('LDAP misconfiguration. LDAP PHP Module is not installed.', RackTablesError::MISCONFIGURED);
483
484 $ldap_cant_connect_codes = array
485 (
486 -1, // Can't contact LDAP server error
487 -5, // LDAP Timed out error
488 -11, // LDAP connect error
489 );
490
491 $last_successful_server = loadScript ('LDAPLastSuccessfulServer');
492 $success_server = NULL;
493 $servers = preg_split ("/\s+/", $LDAP_options['server'], NULL, PREG_SPLIT_NO_EMPTY);
494 if (isset ($last_successful_server) && in_array ($last_successful_server, $servers)) // Cached server is still present in config ?
495 {
496 // Use last successful server first
497 $servers = array_diff ($servers, array ($last_successful_server));
498 array_unshift ($servers, $last_successful_server);
499 }
500 // Try to connect to each server until first success
501 foreach ($servers as $server)
502 {
503 $connect = @ldap_connect ($server, array_fetch ($LDAP_options, 'port', 389));
504 if ($connect === FALSE)
505 continue;
506 ldap_set_option ($connect, LDAP_OPT_NETWORK_TIMEOUT, array_fetch ($LDAP_options, 'server_alive_timeout', 2));
507 // If use_tls configuration option is set, then try establish TLS session instead of ldap_bind
508 if (isset ($LDAP_options['use_tls']) && $LDAP_options['use_tls'] >= 1)
509 {
510 $tls = ldap_start_tls ($connect);
511 if ($LDAP_options['use_tls'] >= 2 && $tls == FALSE)
512 {
513 if (in_array (ldap_errno ($connect), $ldap_cant_connect_codes))
514 continue;
515 else
516 throw new RackTablesError ('LDAP misconfiguration: LDAP TLS required but not successfully negotiated.', RackTablesError::MISCONFIGURED);
517 }
518 $success_server = $server;
519 break;
520 }
521 else
522 {
523 if (@ldap_bind ($connect) || !in_array (ldap_errno ($connect), $ldap_cant_connect_codes))
524 {
525 $success_server = $server;
526 // Cleanup after check. This connection will be used below
527 @ldap_unbind ($connect);
528 $connect = ldap_connect ($server, array_fetch ($LDAP_options, 'port', 389));
529 break;
530 }
531 }
532 }
533 if (!isset ($success_server))
534 return array ('result' => 'CAN');
535 if ($LDAP_options['cache_expiry'] != 0 &&
536 $last_successful_server !== $success_server)
537 saveScript ('LDAPLastSuccessfulServer', $success_server);
538
539 if (array_key_exists ('options', $LDAP_options) && is_array ($LDAP_options['options']))
540 foreach ($LDAP_options['options'] as $opt_code => $opt_value)
541 ldap_set_option ($connect, $opt_code, $opt_value);
542
543 // Decide on the username we will actually authenticate for.
544 if (isset ($LDAP_options['domain']) && $LDAP_options['domain'] != '')
545 $auth_user_name = $username . "@" . $LDAP_options['domain'];
546 elseif
547 (
548 isset ($LDAP_options['search_dn']) &&
549 $LDAP_options['search_dn'] != '' &&
550 isset ($LDAP_options['search_attr']) &&
551 $LDAP_options['search_attr'] != ''
552 )
553 {
554 // If a search_bind_rdn is supplied, bind to that and use it to search.
555 // This is required unless a server offers anonymous searching.
556 // Using bind again on the connection works as expected.
557 // The password is optional as it might be optional on server, too.
558 if (isset ($LDAP_options['search_bind_rdn']) && $LDAP_options['search_bind_rdn'] != '')
559 {
560 $search_bind = @ldap_bind
561 (
562 $connect,
563 $LDAP_options['search_bind_rdn'],
564 isset ($LDAP_options['search_bind_password']) ? $LDAP_options['search_bind_password'] : NULL
565 );
566 if ($search_bind === FALSE)
567 throw new RackTablesError
568 (
569 'LDAP misconfiguration. You have specified a search_bind_rdn ' .
570 (isset ($LDAP_options['search_bind_password']) ? 'with' : 'without') .
571 ' a search_bind_password, but the server refused it with: ' . ldap_error ($connect),
572 RackTablesError::MISCONFIGURED
573 );
574 }
575 $results = @ldap_search ($connect, $LDAP_options['search_dn'], '(' . $LDAP_options['search_attr'] . "=${username})", array("dn"));
576 if ($results === FALSE)
577 return array ('result' => 'CAN');
578 if (@ldap_count_entries ($connect, $results) != 1)
579 {
580 @ldap_close ($connect);
581 return array ('result' => 'NAK');
582 }
583 $info = @ldap_get_entries ($connect, $results);
584 ldap_free_result ($results);
585 $auth_user_name = $info[0]['dn'];
586 }
587 else
588 throw new RackTablesError ('LDAP misconfiguration. Cannon build username for authentication.', RackTablesError::MISCONFIGURED);
589 $bind = @ldap_bind ($connect, $auth_user_name, $password);
590 if ($bind === FALSE)
591 switch (ldap_errno ($connect))
592 {
593 case 49: // LDAP_INVALID_CREDENTIALS
594 return array ('result' => 'NAK');
595 default:
596 return array ('result' => 'CAN');
597 }
598 // preliminary decision may change during searching
599 $ret = array ('result' => 'ACK', 'displayed_name' => '', 'memberof' => array());
600 // Some servers deny anonymous search, thus search (if requested) only after binding.
601 // Displayed name only makes sense for authenticated users anyway.
602 if
603 (
604 isset ($LDAP_options['displayname_attrs']) &&
605 $LDAP_options['displayname_attrs'] != '' &&
606 isset ($LDAP_options['search_dn']) &&
607 $LDAP_options['search_dn'] != '' &&
608 isset ($LDAP_options['search_attr']) &&
609 $LDAP_options['search_attr'] != ''
610 )
611 {
612 $results = @ldap_search
613 (
614 $connect,
615 $LDAP_options['search_dn'],
616 '(' . $LDAP_options['search_attr'] . "=${username})",
617 array_merge (array ($LDAP_options['group_attr']), explode (' ', $LDAP_options['displayname_attrs']))
618 );
619 if (@ldap_count_entries ($connect, $results) != 1)
620 {
621 @ldap_close ($connect);
622 return array ('result' => 'NAK');
623 }
624 $info = @ldap_get_entries ($connect, $results);
625 ldap_free_result ($results);
626 $space = '';
627 foreach (explode (' ', $LDAP_options['displayname_attrs']) as $attr)
628 if (isset ($info[0][$attr]))
629 {
630 $ret['displayed_name'] .= $space . $info[0][$attr][0];
631 $space = ' ';
632 }
633 // Pull group membership, if any was returned.
634 if (isset ($info[0][$LDAP_options['group_attr']]))
635 for ($i = 0; $i < $info[0][$LDAP_options['group_attr']]['count']; $i++)
636 if
637 (
638 preg_match ($LDAP_options['group_filter'], $info[0][$LDAP_options['group_attr']][$i], $matches) &&
639 validTagName ('$lgcn_' . $matches[1], TRUE)
640 )
641 $ret['memberof'][] = '$lgcn_' . $matches[1];
642 }
643 @ldap_close ($connect);
644 return $ret;
645 }
646
647 function authenticated_via_database ($userinfo, $password)
648 {
649 if (!isset ($userinfo['user_id'])) // not a local account
650 return FALSE;
651 return $userinfo['user_password_hash'] == sha1 ($password);
652 }
653
654 ?>