refine the use of $debug_mode
[racktables] / wwwroot / inc / auth.php
CommitLineData
b325120a 1<?php
cddbb9fd
DO
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
e673ee24
DO
7/*
8
7952f733
DO
9Below is a mix of authentication (confirming user's identity) and authorization
10(access controlling) functions of RackTables. The former set is expected to
11be working with only database.php file included.
e673ee24
DO
12
13*/
14
15// This function ensures that we don't continue without a legitimate
d4d873be
DO
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
b82cce3f 18// anonymous binding). It also initializes $remote_* and $*_tags vars.
e673ee24
DO
19function authenticate ()
20{
e926b0c2
DO
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
b82cce3f
DO
33 global
34 $remote_username,
35 $remote_displayname,
36 $auto_tags,
37 $user_given_tags,
38 $user_auth_src,
44869431 39 $script_mode,
b82cce3f 40 $require_local_account;
a8ae6f95 41 // Phase 1. Assert basic pre-requisites, short-circuit the logout request.
55eefced 42 if (! isset ($user_auth_src) || ! isset ($require_local_account))
3a089a44 43 throw new RackTablesError ('secret.php: either user_auth_src or require_local_account are missing', RackTablesError::MISCONFIGURED);
4c4106f2 44 if (isset ($_REQUEST['logout']))
1338ab01 45 {
55eefced 46 if (isset ($user_auth_src) && 'saml' == $user_auth_src)
1338ab01 47 saml_logout ();
3a089a44 48 throw new RackTablesError ('', RackTablesError::NOT_AUTHENTICATED); // Reset browser credentials cache.
1338ab01 49 }
a8ae6f95 50 // Phase 2. Do some method-specific processing, initialize $remote_username on success.
143b9c99
DO
51 switch (TRUE)
52 {
5aab5cd6 53 case isset ($script_mode) && $script_mode && isset ($remote_username) && $remote_username != '':
143b9c99
DO
54 break; // skip this phase
55 case 'database' == $user_auth_src:
e926b0c2
DO
56 assertHTTPCredentialsReceived();
57 $remote_username = $_SERVER['PHP_AUTH_USER'];
58 break;
143b9c99 59 case 'ldap' == $user_auth_src:
e926b0c2 60 assertHTTPCredentialsReceived();
143b9c99 61 $remote_username = $_SERVER['PHP_AUTH_USER'];
07a10e00 62 constructLDAPOptions();
143b9c99
DO
63 break;
64 case 'httpd' == $user_auth_src:
65 if
66 (
67 ! isset ($_SERVER['REMOTE_USER']) or
5aab5cd6 68 $_SERVER['REMOTE_USER'] == ''
143b9c99
DO
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;
6f661b14
MS
73 case 'saml' == $user_auth_src:
74 $saml_username = '';
dc4943d3
DO
75 $saml_dispname = '';
76 if (! authenticated_via_saml ($saml_username, $saml_dispname))
77 throw new RackTablesError ('', RackTablesError::NOT_AUTHENTICATED);
6f661b14
MS
78 $remote_username = $saml_username;
79 break;
143b9c99
DO
80 default:
81 throw new RackTablesError ('Invalid authentication source!', RackTablesError::MISCONFIGURED);
82 }
a8ae6f95 83 // Phase 3. Handle local account requirement, pull user tags into security context.
d16af52f 84 $userinfo = constructUserCell ($remote_username);
55eefced 85 if ($require_local_account && ! isset ($userinfo['user_id']))
3a089a44 86 throw new RackTablesError ('', RackTablesError::NOT_AUTHENTICATED);
d16af52f
DO
87 $user_given_tags = $userinfo['etags'];
88 $auto_tags = array_merge ($auto_tags, $userinfo['atags']);
a8ae6f95 89 // Phase 4. Do more method-specific processing, initialize $remote_displayname on success.
dc9ea133
DO
90 switch (TRUE)
91 {
44869431
AA
92 case isset ($script_mode) && $script_mode:
93 return; // success
dc9ea133 94 // Just trust the server, because the password isn't known.
143b9c99 95 case 'httpd' == $user_auth_src:
5aab5cd6 96 $remote_displayname = $userinfo['user_realname'] != '' ?
1d34465d
DO
97 $userinfo['user_realname'] :
98 $remote_username;
99 return; // success
dc9ea133 100 // When using LDAP, leave a mean to fix things. Admin user is always authenticated locally.
55eefced 101 case array_key_exists ('user_id', $userinfo) && $userinfo['user_id'] == 1:
143b9c99 102 case 'database' == $user_auth_src:
5aab5cd6 103 $remote_displayname = $userinfo['user_realname'] != '' ?
1d34465d
DO
104 $userinfo['user_realname'] :
105 $remote_username;
d16af52f 106 if (authenticated_via_database ($userinfo, $_SERVER['PHP_AUTH_PW']))
1d34465d
DO
107 return; // success
108 break; // failure
143b9c99 109 case 'ldap' == $user_auth_src:
1d34465d 110 $ldap_dispname = '';
e89eb2e3 111 if (! authenticated_via_ldap ($remote_username, $_SERVER['PHP_AUTH_PW'], $ldap_dispname))
1d34465d 112 break; // failure
5aab5cd6 113 $remote_displayname = $userinfo['user_realname'] != '' ? // local value is most preferred
1d34465d 114 $userinfo['user_realname'] :
5aab5cd6 115 ($ldap_dispname != '' ? $ldap_dispname : $remote_username); // then one from LDAP
1d34465d 116 return; // success
6f661b14 117 case 'saml' == $user_auth_src:
5aab5cd6 118 $remote_displayname = $saml_dispname != '' ? $saml_dispname : $saml_username;
6f661b14 119 return; // success
dc9ea133 120 default:
3a089a44 121 throw new RackTablesError ('Invalid authentication source!', RackTablesError::MISCONFIGURED);
dc9ea133 122 }
3a089a44 123 throw new RackTablesError ('', RackTablesError::NOT_AUTHENTICATED);
e673ee24
DO
124}
125
da958e52
DO
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.
46f92ff7 129function permitted ($p = NULL, $t = NULL, $o = NULL, $annex = array())
e673ee24 130{
da958e52 131 global $pageno, $tabno, $op;
7ddb2c05 132 global $auto_tags;
da958e52
DO
133
134 if ($p === NULL)
135 $p = $pageno;
136 if ($t === NULL)
137 $t = $tabno;
55eefced 138 if ($o === NULL && $op != '') // $op can be set to empty string
b46d20f5 139 $o = $op;
00f887f4
DO
140 $my_auto_tags = $auto_tags;
141 $my_auto_tags[] = array ('tag' => '$page_' . $p);
142 $my_auto_tags[] = array ('tag' => '$tab_' . $t);
b46d20f5 143 if ($o !== NULL) // these tags only make sense in certain cases
00f887f4 144 {
b46d20f5 145 $my_auto_tags[] = array ('tag' => '$op_' . $o);
00f887f4
DO
146 $my_auto_tags[] = array ('tag' => '$any_op');
147 }
da958e52
DO
148 $subject = array_merge
149 (
00f887f4 150 $my_auto_tags,
46f92ff7 151 $annex
da958e52 152 );
1c9621a7
DO
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".
da958e52 178 return gotClearanceForTagChain ($subject);
e673ee24
DO
179}
180
3ec33017
DO
181# a "throwing" wrapper for above
182function assertPermission ($p = NULL, $t = NULL, $o = NULL, $annex = array())
183{
184 if (! permitted ($p, $t, $o, $annex))
185 throw new RTPermissionDenied();
186}
187
277dd019
DO
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.
ef0503fc
DO
196function gotClearanceForTagChain ($const_base)
197{
198 global $rackCode, $expl_tags, $impl_tags;
cf416339 199 $context = array_merge ($const_base, $expl_tags, $impl_tags);
f3685414 200 $context = reindexById ($context, 'tag', TRUE);
cf416339 201
ef0503fc
DO
202 foreach ($rackCode as $sentence)
203 {
204 switch ($sentence['type'])
205 {
ef0503fc 206 case 'SYNT_GRANT':
3420bb6b 207 if (eval_expression ($sentence['condition'], $context))
5225db53 208 return $sentence['decision'];
ef0503fc
DO
209 break;
210 case 'SYNT_ADJUSTMENT':
211 if
212 (
55eefced 213 eval_expression ($sentence['condition'], $context) &&
ef0503fc
DO
214 processAdjustmentSentence ($sentence['modlist'], $expl_tags)
215 ) // recalculate implicit chain only after actual change, not just on matched condition
f939bbdf 216 {
ef0503fc 217 $impl_tags = getImplicitTags ($expl_tags); // recalculate
f939bbdf 218 $context = array_merge ($const_base, $expl_tags, $impl_tags);
f3685414 219 $context = reindexById ($context, 'tag', TRUE);
f939bbdf 220 }
ef0503fc
DO
221 break;
222 default:
dca557e5 223 throw new RackTablesError ("Can't process sentence of unknown type '${sentence['type']}'", RackTablesError::INTERNAL);
ef0503fc
DO
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.
233function 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
6f661b14
MS
269// a wrapper for SAML auth method
270function authenticated_via_saml (&$saml_username = NULL, &$saml_displayname = NULL)
271{
e8d843f4 272 global $SAML_options, $auto_tags;
6f661b14
MS
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']);
1338ab01 282 if (array_key_exists ('groupListAttribute', $SAML_options))
1338ab01
TP
283 foreach (saml_getAttributeValues ($attributes, $SAML_options['groupListAttribute']) as $autotag)
284 $auto_tags[] = array ('tag' => '$sgcn_' . $autotag);
dc4943d3 285 return $as->isAuthenticated();
6f661b14
MS
286}
287
1338ab01
TP
288function 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
6f661b14
MS
299function saml_getAttributeValue ($attributes, $name)
300{
dc4943d3
DO
301 if (! isset ($attributes[$name]))
302 return '';
303 return is_array ($attributes[$name]) ? $attributes[$name][0] : $attributes[$name];
6f661b14
MS
304}
305
1338ab01
TP
306function 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
07a10e00 313function constructLDAPOptions()
7c963251 314{
07a10e00
DO
315 global $LDAP_options;
316 if (! isset ($LDAP_options))
317 throw new RackTablesError ('$LDAP_options has not been defined (see secret.php)', RackTablesError::MISCONFIGURED);
88a92748
AD
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;
07a10e00 329}
88a92748 330
07a10e00
DO
331// a wrapper for two LDAP auth methods below
332function authenticated_via_ldap ($username, $password, &$ldap_displayname)
333{
334 global $LDAP_options, $debug_mode;
1f54e1ba
DO
335 try
336 {
70057f43 337 // Destroy the cache each time config changes.
02b77d4c
AA
338 if ($LDAP_options['cache_expiry'] != 0 &&
339 sha1 (serialize ($LDAP_options)) != loadScript ('LDAPConfigHash'))
70057f43 340 {
341 discardLDAPCache();
342 saveScript ('LDAPConfigHash', sha1 (serialize ($LDAP_options)));
343 deleteScript ('LDAPLastSuccessfulServer');
344 }
345
346 if
347 (
55eefced 348 $LDAP_options['cache_retry'] > $LDAP_options['cache_refresh'] ||
70057f43 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.
1f54e1ba
DO
358 return authenticated_via_ldap_cache ($username, $password, $ldap_displayname);
359 }
360 catch (PDOException $e)
361 {
cde6cec3
AA
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);
1f54e1ba 368 }
7c963251
DO
369}
370
371// Authenticate given user with known LDAP server, completely ignore LDAP cache data.
1d34465d 372function authenticated_via_ldap_nocache ($username, $password, &$ldap_displayname)
7c963251 373{
1d34465d 374 global $auto_tags;
7c963251
DO
375 $server_test = queryLDAPServer ($username, $password);
376 if ($server_test['result'] == 'ACK')
377 {
1d34465d 378 $ldap_displayname = $server_test['displayed_name'];
7c963251
DO
379 foreach ($server_test['memberof'] as $autotag)
380 $auto_tags[] = array ('tag' => $autotag);
381 return TRUE;
382 }
383 return FALSE;
384}
385
4ddcded8
AA
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
388function 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
7c963251
DO
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.
1d34465d 411function authenticated_via_ldap_cache ($username, $password, &$ldap_displayname)
9133d2c5 412{
1d34465d 413 global $LDAP_options, $auto_tags;
9133d2c5 414
4ddcded8
AA
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
9133d2c5 423 {
4ddcded8
AA
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
9133d2c5 429 {
4ddcded8
AA
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 }
9133d2c5
DO
453 }
454 releaseLDAPCache();
9133d2c5 455 }
4ddcded8
AA
456
457 if ($user_data)
9133d2c5 458 {
4ddcded8
AA
459 $ldap_displayname = $user_data['displayed_name'];
460 foreach ($user_data['memberof'] as $autotag)
9133d2c5 461 $auto_tags[] = array ('tag' => $autotag);
9133d2c5 462 return TRUE;
9133d2c5 463 }
9133d2c5
DO
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
ef44d4a3 474// 'displayed_name' : a string built according to LDAP displayname_attrs option
9133d2c5
DO
475// 'memberof' => filtered list of all LDAP groups the user belongs to
476//
477function queryLDAPServer ($username, $password)
7dfd5e44 478{
ef44d4a3 479 global $LDAP_options;
9133d2c5 480
0a26c1ec 481 if (extension_loaded ('ldap') === FALSE)
7dbf4617 482 throw new RackTablesError ('LDAP misconfiguration. LDAP PHP Module is not installed.', RackTablesError::MISCONFIGURED);
dec748f6 483
0a26c1ec
DO
484 $ldap_cant_connect_codes = array
485 (
70057f43 486 -1, // Can't contact LDAP server error
487 -5, // LDAP Timed out error
488 -11, // LDAP connect error
489 );
9133d2c5 490
70057f43 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)
2f9a653f 502 {
70057f43 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 {
0a26c1ec 513 if (in_array (ldap_errno ($connect), $ldap_cant_connect_codes))
70057f43 514 continue;
515 else
516 throw new RackTablesError ('LDAP misconfiguration: LDAP TLS required but not successfully negotiated.', RackTablesError::MISCONFIGURED);
517 }
3207ebb0 518 $success_server = $server;
d0a0d331 519 break;
70057f43 520 }
521 else
522 {
0a26c1ec 523 if (@ldap_bind ($connect) || !in_array (ldap_errno ($connect), $ldap_cant_connect_codes))
70057f43 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 }
2f9a653f 532 }
70057f43 533 if (!isset ($success_server))
534 return array ('result' => 'CAN');
02b77d4c
AA
535 if ($LDAP_options['cache_expiry'] != 0 &&
536 $last_successful_server !== $success_server)
70057f43 537 saveScript ('LDAPLastSuccessfulServer', $success_server);
2f9a653f 538
55eefced 539 if (array_key_exists ('options', $LDAP_options) && is_array ($LDAP_options['options']))
b6556a7a
DO
540 foreach ($LDAP_options['options'] as $opt_code => $opt_value)
541 ldap_set_option ($connect, $opt_code, $opt_value);
542
9133d2c5 543 // Decide on the username we will actually authenticate for.
55eefced 544 if (isset ($LDAP_options['domain']) && $LDAP_options['domain'] != '')
ef44d4a3 545 $auth_user_name = $username . "@" . $LDAP_options['domain'];
9133d2c5
DO
546 elseif
547 (
55eefced
DO
548 isset ($LDAP_options['search_dn']) &&
549 $LDAP_options['search_dn'] != '' &&
550 isset ($LDAP_options['search_attr']) &&
5aab5cd6 551 $LDAP_options['search_attr'] != ''
9133d2c5 552 )
8c3bd904 553 {
b6556a7a
DO
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.
5aab5cd6 558 if (isset ($LDAP_options['search_bind_rdn']) && $LDAP_options['search_bind_rdn'] != '')
b6556a7a
DO
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 }
ef44d4a3 575 $results = @ldap_search ($connect, $LDAP_options['search_dn'], '(' . $LDAP_options['search_attr'] . "=${username})", array("dn"));
9133d2c5
DO
576 if ($results === FALSE)
577 return array ('result' => 'CAN');
578 if (@ldap_count_entries ($connect, $results) != 1)
8c3bd904 579 {
9133d2c5
DO
580 @ldap_close ($connect);
581 return array ('result' => 'NAK');
d6d79c36 582 }
9133d2c5
DO
583 $info = @ldap_get_entries ($connect, $results);
584 ldap_free_result ($results);
585 $auth_user_name = $info[0]['dn'];
586 }
587 else
3a089a44 588 throw new RackTablesError ('LDAP misconfiguration. Cannon build username for authentication.', RackTablesError::MISCONFIGURED);
9133d2c5
DO
589 $bind = @ldap_bind ($connect, $auth_user_name, $password);
590 if ($bind === FALSE)
591 switch (ldap_errno ($connect))
d6d79c36 592 {
9133d2c5
DO
593 case 49: // LDAP_INVALID_CREDENTIALS
594 return array ('result' => 'NAK');
595 default:
596 return array ('result' => 'CAN');
8c3bd904 597 }
9133d2c5
DO
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 (
55eefced
DO
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']) &&
5aab5cd6 609 $LDAP_options['search_attr'] != ''
9133d2c5
DO
610 )
611 {
612 $results = @ldap_search
613 (
614 $connect,
ef44d4a3
DO
615 $LDAP_options['search_dn'],
616 '(' . $LDAP_options['search_attr'] . "=${username})",
9bce2cce 617 array_merge (array ($LDAP_options['group_attr']), explode (' ', $LDAP_options['displayname_attrs']))
9133d2c5
DO
618 );
619 if (@ldap_count_entries ($connect, $results) != 1)
ae65938e
DO
620 {
621 @ldap_close ($connect);
9133d2c5 622 return array ('result' => 'NAK');
ae65938e 623 }
9133d2c5
DO
624 $info = @ldap_get_entries ($connect, $results);
625 ldap_free_result ($results);
626 $space = '';
ef44d4a3 627 foreach (explode (' ', $LDAP_options['displayname_attrs']) as $attr)
3679a75d
AA
628 if (isset ($info[0][$attr]))
629 {
630 $ret['displayed_name'] .= $space . $info[0][$attr][0];
631 $space = ' ';
632 }
9133d2c5 633 // Pull group membership, if any was returned.
9bce2cce
DO
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 (
55eefced
DO
638 preg_match ($LDAP_options['group_filter'], $info[0][$LDAP_options['group_attr']][$i], $matches) &&
639 validTagName ('$lgcn_' . $matches[1], TRUE)
9bce2cce
DO
640 )
641 $ret['memberof'][] = '$lgcn_' . $matches[1];
8c3bd904 642 }
ae65938e 643 @ldap_close ($connect);
9133d2c5 644 return $ret;
7dfd5e44
DO
645}
646
d16af52f 647function authenticated_via_database ($userinfo, $password)
7dfd5e44 648{
d16af52f 649 if (!isset ($userinfo['user_id'])) // not a local account
b82cce3f 650 return FALSE;
f3371850 651 return $userinfo['user_password_hash'] == sha1 ($password);
e673ee24
DO
652}
653
e673ee24 654?>