r4367 auth.php: update comment
[racktables] / wwwroot / inc / auth.php
CommitLineData
b325120a 1<?php
e673ee24
DO
2/*
3
7952f733
DO
4Below is a mix of authentication (confirming user's identity) and authorization
5(access controlling) functions of RackTables. The former set is expected to
6be working with only database.php file included.
e673ee24
DO
7
8*/
9
10// This function ensures that we don't continue without a legitimate
d4d873be
DO
11// username and password (also make sure, that both are present, this
12// is especially useful for LDAP auth code to not deceive itself with
b82cce3f 13// anonymous binding). It also initializes $remote_* and $*_tags vars.
e673ee24
DO
14function authenticate ()
15{
b82cce3f
DO
16 global
17 $remote_username,
18 $remote_displayname,
19 $auto_tags,
20 $user_given_tags,
21 $user_auth_src,
22 $require_local_account;
23 if (!isset ($user_auth_src) or !isset ($require_local_account))
3a089a44 24 throw new RackTablesError ('secret.php: either user_auth_src or require_local_account are missing', RackTablesError::MISCONFIGURED);
dc9ea133 25 if (isset ($_REQUEST['logout']))
3a089a44 26 throw new RackTablesError ('', RackTablesError::NOT_AUTHENTICATED); // Reset browser credentials cache.
204284ba 27 switch ($user_auth_src)
e673ee24 28 {
dc9ea133
DO
29 case 'database':
30 case 'ldap':
31 if
32 (
33 !isset ($_SERVER['PHP_AUTH_USER']) or
34 !strlen ($_SERVER['PHP_AUTH_USER']) or
35 !isset ($_SERVER['PHP_AUTH_PW']) or
36 !strlen ($_SERVER['PHP_AUTH_PW'])
37 )
3a089a44 38 throw new RackTablesError ('', RackTablesError::NOT_AUTHENTICATED);
dc9ea133
DO
39 $remote_username = $_SERVER['PHP_AUTH_USER'];
40 break;
41 case 'httpd':
42 if
43 (
44 !isset ($_SERVER['REMOTE_USER']) or
45 !strlen ($_SERVER['REMOTE_USER'])
46 )
3a089a44 47 throw new RackTablesError ('The web-server didn\'t authenticate the user, although ought to do.', RackTablesError::MISCONFIGURED);
dc9ea133
DO
48 $remote_username = $_SERVER['REMOTE_USER'];
49 break;
50 default:
3a089a44 51 throw new RackTablesError ('Invalid authentication source!', RackTablesError::MISCONFIGURED);
dc9ea133 52 die;
e673ee24 53 }
d16af52f
DO
54 $userinfo = constructUserCell ($remote_username);
55 if ($require_local_account and !isset ($userinfo['user_id']))
3a089a44 56 throw new RackTablesError ('', RackTablesError::NOT_AUTHENTICATED);
d16af52f
DO
57 $user_given_tags = $userinfo['etags'];
58 $auto_tags = array_merge ($auto_tags, $userinfo['atags']);
dc9ea133
DO
59 switch (TRUE)
60 {
61 // Just trust the server, because the password isn't known.
204284ba 62 case ('httpd' == $user_auth_src):
1d34465d
DO
63 $remote_displayname = strlen ($userinfo['user_realname']) ?
64 $userinfo['user_realname'] :
65 $remote_username;
66 return; // success
dc9ea133 67 // When using LDAP, leave a mean to fix things. Admin user is always authenticated locally.
ea17c8b9 68 case ('database' == $user_auth_src or (array_key_exists ('user_id', $userinfo) and $userinfo['user_id'] == 1)):
1d34465d
DO
69 $remote_displayname = strlen ($userinfo['user_realname']) ?
70 $userinfo['user_realname'] :
71 $remote_username;
d16af52f 72 if (authenticated_via_database ($userinfo, $_SERVER['PHP_AUTH_PW']))
1d34465d
DO
73 return; // success
74 break; // failure
204284ba 75 case ('ldap' == $user_auth_src):
1d34465d
DO
76 $ldap_dispname = '';
77 $ldap_success = authenticated_via_ldap ($remote_username, $_SERVER['PHP_AUTH_PW'], $ldap_dispname);
78 if (!$ldap_success)
79 break; // failure
80 $remote_displayname = strlen ($userinfo['user_realname']) ? // local value is most preferred
81 $userinfo['user_realname'] :
82 (strlen ($ldap_dispname) ? $ldap_dispname : $remote_username); // then one from LDAP
83 return; // success
dc9ea133 84 default:
3a089a44 85 throw new RackTablesError ('Invalid authentication source!', RackTablesError::MISCONFIGURED);
dc9ea133 86 }
3a089a44 87 throw new RackTablesError ('', RackTablesError::NOT_AUTHENTICATED);
e673ee24
DO
88}
89
da958e52
DO
90// Merge accumulated tags into a single chain, add location-specific
91// autotags and try getting access clearance. Page and tab are mandatory,
92// operation is optional.
46f92ff7 93function permitted ($p = NULL, $t = NULL, $o = NULL, $annex = array())
e673ee24 94{
da958e52 95 global $pageno, $tabno, $op;
7ddb2c05 96 global $auto_tags;
da958e52
DO
97
98 if ($p === NULL)
99 $p = $pageno;
100 if ($t === NULL)
101 $t = $tabno;
b46d20f5
DO
102 if ($o === NULL and strlen ($op)) // $op can be set to empty string
103 $o = $op;
00f887f4
DO
104 $my_auto_tags = $auto_tags;
105 $my_auto_tags[] = array ('tag' => '$page_' . $p);
106 $my_auto_tags[] = array ('tag' => '$tab_' . $t);
b46d20f5 107 if ($o !== NULL) // these tags only make sense in certain cases
00f887f4 108 {
b46d20f5 109 $my_auto_tags[] = array ('tag' => '$op_' . $o);
00f887f4
DO
110 $my_auto_tags[] = array ('tag' => '$any_op');
111 }
da958e52
DO
112 $subject = array_merge
113 (
00f887f4 114 $my_auto_tags,
46f92ff7 115 $annex
da958e52 116 );
1c9621a7
DO
117 // XXX: The solution below is only appropriate for a corner case of a more universal
118 // problem: to make the decision for an entity belonging to a cascade of nested
119 // containers. Each container being an entity itself, it may have own tags (explicit
120 // and implicit accordingly). There's a fixed set of rules (RackCode) with each rule
121 // being able to evaluate any built and given context and produce either a decision
122 // or a lack of decision.
123 // There are several levels of context for the target entity, at least one for entities
124 // belonging directly to the tree root. Each level's context is a union of given
125 // container's tags and the tags of the contained entities.
126 // The universal problem originates from the fact, that certain rules may change
127 // their product as context level changes, thus forcing some final decision (but not
128 // adding a lack of it). With rule code being principles and context cascade being
129 // circumstances, there are two uttermost approaches or moralities.
130 //
131 // Fundamentalism: principles over circumstances. When a rule doesn't produce any
132 // decision, go on to the next rule. When all rules are evaluated, go on to the next
133 // security context level.
134 //
135 // Opportunism: circumstances over principles. With a lack of decision, work with the
136 // same rule, trying to evaluate it against the next level (and next, and next...),
137 // until all levels are tried. Only then go on to the next rule.
138 //
139 // With the above being simple discrete algorythms, I believe, that they very reliably
140 // replicate human behavior. This gives a vast ground for further research, so I would
141 // only note, that the morale used in RackTables is "principles first".
da958e52 142 return gotClearanceForTagChain ($subject);
e673ee24
DO
143}
144
3ec33017
DO
145# a "throwing" wrapper for above
146function assertPermission ($p = NULL, $t = NULL, $o = NULL, $annex = array())
147{
148 if (! permitted ($p, $t, $o, $annex))
149 throw new RTPermissionDenied();
150}
151
ef0503fc
DO
152// The argument doesn't include explicit and implicit tags. This allows us to derive implicit chain
153// each time we modify the given argument (and work with the modified copy from now on).
154// After the work is done the global $impl_tags is silently modified
155function gotClearanceForTagChain ($const_base)
156{
157 global $rackCode, $expl_tags, $impl_tags;
158 $ptable = array();
159 foreach ($rackCode as $sentence)
160 {
161 switch ($sentence['type'])
162 {
163 case 'SYNT_DEFINITION':
164 $ptable[$sentence['term']] = $sentence['definition'];
165 break;
166 case 'SYNT_GRANT':
167 if (eval_expression ($sentence['condition'], array_merge ($const_base, $expl_tags, $impl_tags), $ptable))
168 switch ($sentence['decision'])
169 {
170 case 'LEX_ALLOW':
171 return TRUE;
172 case 'LEX_DENY':
173 return FALSE;
174 default:
dca557e5 175 throw new RackTablesError ("Condition match for unknown grant decision '${sentence['decision']}'", RackTablesError::INTERNAL);
ef0503fc
DO
176 }
177 break;
178 case 'SYNT_ADJUSTMENT':
179 if
180 (
181 eval_expression ($sentence['condition'], array_merge ($const_base, $expl_tags, $impl_tags), $ptable) and
182 processAdjustmentSentence ($sentence['modlist'], $expl_tags)
183 ) // recalculate implicit chain only after actual change, not just on matched condition
184 $impl_tags = getImplicitTags ($expl_tags); // recalculate
185 break;
186 default:
dca557e5 187 throw new RackTablesError ("Can't process sentence of unknown type '${sentence['type']}'", RackTablesError::INTERNAL);
ef0503fc
DO
188 }
189 }
190 return FALSE;
191}
192
193// Process a context adjustment request, update given chain accordingly,
194// return TRUE on any changes done.
195// The request is a sequence of clear/insert/remove requests exactly as cooked
196// for each SYNT_CTXMODLIST node.
197function processAdjustmentSentence ($modlist, &$chain)
198{
199 global $rackCode;
200 $didChanges = FALSE;
201 foreach ($modlist as $mod)
202 switch ($mod['op'])
203 {
204 case 'insert':
205 foreach ($chain as $etag)
206 if ($etag['tag'] == $mod['tag']) // already there, next request
207 break 2;
208 $search = getTagByName ($mod['tag']);
209 if ($search === NULL) // skip martians silently
210 break;
211 $chain[] = $search;
212 $didChanges = TRUE;
213 break;
214 case 'remove':
215 foreach ($chain as $key => $etag)
216 if ($etag['tag'] == $mod['tag']) // drop first match and return
217 {
218 unset ($chain[$key]);
219 $didChanges = TRUE;
220 break 2;
221 }
222 break;
223 case 'clear':
224 $chain = array();
225 $didChanges = TRUE;
226 break;
227 default: // HCF
228 throw new RackTablesError ('invalid structure', RackTablesError::INTERNAL);
229 }
230 return $didChanges;
231}
232
7c963251 233// a wrapper for two LDAP auth methods below
1d34465d 234function authenticated_via_ldap ($username, $password, &$ldap_displayname)
7c963251
DO
235{
236 global $LDAP_options;
6b06a019
DO
237 if
238 (
7c963251
DO
239 $LDAP_options['cache_retry'] > $LDAP_options['cache_refresh'] or
240 $LDAP_options['cache_refresh'] > $LDAP_options['cache_expiry']
241 )
6b06a019 242 throw new RackTablesError ('LDAP misconfiguration: refresh/retry/expiry mismatch', RackTablesError::MISCONFIGURED);
7c963251 243 if ($LDAP_options['cache_expiry'] == 0) // immediate expiry set means disabled cache
1d34465d 244 return authenticated_via_ldap_nocache ($username, $password, $ldap_displayname);
1f54e1ba
DO
245 // authenticated_via_ldap_cache()'s way of locking can sometimes result in
246 // a PDO error condition, which convertPDOException() was not able to dispatch.
247 // To avoid reaching printPDOException() (which prints backtrace with password
248 // argument in cleartext), any remaining PDO condition is converted locally.
249 try
250 {
251 return authenticated_via_ldap_cache ($username, $password, $ldap_displayname);
252 }
253 catch (PDOException $e)
254 {
255 throw new RackTablesError ('LDAP caching error', RackTablesError::DB_WRITE_FAILED);
256 }
7c963251
DO
257}
258
259// Authenticate given user with known LDAP server, completely ignore LDAP cache data.
1d34465d 260function authenticated_via_ldap_nocache ($username, $password, &$ldap_displayname)
7c963251 261{
1d34465d 262 global $auto_tags;
7c963251
DO
263 $server_test = queryLDAPServer ($username, $password);
264 if ($server_test['result'] == 'ACK')
265 {
1d34465d 266 $ldap_displayname = $server_test['displayed_name'];
7c963251
DO
267 foreach ($server_test['memberof'] as $autotag)
268 $auto_tags[] = array ('tag' => $autotag);
269 return TRUE;
270 }
271 return FALSE;
272}
273
274// Idem, but consider existing data in cache and modify/discard it, when necessary.
275// Remember to have releaseLDAPCache() called before any return statement.
276// Perform cache maintenance on each update.
1d34465d 277function authenticated_via_ldap_cache ($username, $password, &$ldap_displayname)
9133d2c5 278{
1d34465d 279 global $LDAP_options, $auto_tags;
9133d2c5 280
ef44d4a3
DO
281 // Destroy the cache each time config changes.
282 if (sha1 (serialize ($LDAP_options)) != loadScript ('LDAPConfigHash'))
283 {
284 discardLDAPCache();
285 saveScript ('LDAPConfigHash', sha1 (serialize ($LDAP_options)));
286 }
287 $oldinfo = acquireLDAPCache ($username, sha1 ($password), $LDAP_options['cache_expiry']);
9133d2c5
DO
288 if ($oldinfo === NULL) // cache miss
289 {
290 // On cache miss execute complete procedure and return the result. In case
291 // of successful authentication put a record into cache.
292 $newinfo = queryLDAPServer ($username, $password);
293 if ($newinfo['result'] == 'ACK')
294 {
1d34465d 295 $ldap_displayname = $newinfo['displayed_name'];
9133d2c5
DO
296 foreach ($newinfo['memberof'] as $autotag)
297 $auto_tags[] = array ('tag' => $autotag);
298 replaceLDAPCacheRecord ($username, sha1 ($password), $newinfo['displayed_name'], $newinfo['memberof']);
7c963251
DO
299 releaseLDAPCache();
300 discardLDAPCache ($LDAP_options['cache_expiry']);
301 return TRUE;
9133d2c5
DO
302 }
303 releaseLDAPCache();
7c963251 304 return FALSE;
9133d2c5 305 }
7c963251 306 // cache HIT
9133d2c5
DO
307 // There are two confidence levels of cache hits: "certain" and "uncertain". In either case
308 // expect authentication success, unless it's well-timed to perform a retry,
309 // which may sometimes bring a NAK decision.
ef44d4a3 310 if ($oldinfo['success_age'] < $LDAP_options['cache_refresh'] or $oldinfo['retry_age'] < $LDAP_options['cache_retry'])
9133d2c5
DO
311 {
312 releaseLDAPCache();
1d34465d 313 $ldap_displayname = $oldinfo['displayed_name'];
9133d2c5
DO
314 foreach ($oldinfo['memberof'] as $autotag)
315 $auto_tags[] = array ('tag' => $autotag);
316 return TRUE;
317 }
318 // Either refresh threshold or retry threshold reached.
319 $newinfo = queryLDAPServer ($username, $password);
320 switch ($newinfo['result'])
321 {
322 case 'ACK': // refresh existing record
1d34465d 323 $ldap_displayname = $newinfo['displayed_name'];
9133d2c5
DO
324 foreach ($newinfo['memberof'] as $autotag)
325 $auto_tags[] = array ('tag' => $autotag);
326 replaceLDAPCacheRecord ($username, sha1 ($password), $newinfo['displayed_name'], $newinfo['memberof']);
327 releaseLDAPCache();
7c963251 328 discardLDAPCache ($LDAP_options['cache_expiry']);
9133d2c5
DO
329 return TRUE;
330 case 'NAK': // The record isn't valid any more.
331 deleteLDAPCacheRecord ($username);
332 releaseLDAPCache();
7c963251 333 discardLDAPCache ($LDAP_options['cache_expiry']);
9133d2c5
DO
334 return FALSE;
335 case 'CAN': // retry failed, do nothing, use old value till next retry
1d34465d 336 $ldap_displayname = $oldinfo['displayed_name'];
9133d2c5
DO
337 foreach ($oldinfo['memberof'] as $autotag)
338 $auto_tags[] = array ('tag' => $autotag);
339 touchLDAPCacheRecord ($username);
340 releaseLDAPCache();
7c963251 341 discardLDAPCache ($LDAP_options['cache_expiry']);
9133d2c5
DO
342 return TRUE;
343 default:
7952f733 344 throw new RackTablesError ('structure error', RackTablesError::INTERNAL);
9133d2c5
DO
345 }
346 // This is never reached.
347 return FALSE;
348}
349
350// Attempt a server conversation and return an array describing the outcome:
351//
352// 'result' => 'CAN' : connect (or search) failed completely
353//
354// 'result' => 'NAK' : server replied and denied access (or search returned odd data)
355//
356// 'result' => 'ACK' : server replied and cleared access, there were no search errors
ef44d4a3 357// 'displayed_name' : a string built according to LDAP displayname_attrs option
9133d2c5
DO
358// 'memberof' => filtered list of all LDAP groups the user belongs to
359//
360function queryLDAPServer ($username, $password)
7dfd5e44 361{
ef44d4a3 362 global $LDAP_options;
9133d2c5 363
7dbf4617
DO
364 if(extension_loaded('ldap') === FALSE)
365 throw new RackTablesError ('LDAP misconfiguration. LDAP PHP Module is not installed.', RackTablesError::MISCONFIGURED);
366
ef44d4a3 367 $connect = @ldap_connect ($LDAP_options['server']);
9133d2c5
DO
368 if ($connect === FALSE)
369 return array ('result' => 'CAN');
370
371 // Decide on the username we will actually authenticate for.
59a83bd8 372 if (isset ($LDAP_options['domain']) and strlen ($LDAP_options['domain']))
ef44d4a3 373 $auth_user_name = $username . "@" . $LDAP_options['domain'];
9133d2c5
DO
374 elseif
375 (
ef44d4a3 376 isset ($LDAP_options['search_dn']) and
59a83bd8 377 strlen ($LDAP_options['search_dn']) and
ef44d4a3 378 isset ($LDAP_options['search_attr']) and
59a83bd8 379 strlen ($LDAP_options['search_attr'])
9133d2c5 380 )
8c3bd904 381 {
ef44d4a3 382 $results = @ldap_search ($connect, $LDAP_options['search_dn'], '(' . $LDAP_options['search_attr'] . "=${username})", array("dn"));
9133d2c5
DO
383 if ($results === FALSE)
384 return array ('result' => 'CAN');
385 if (@ldap_count_entries ($connect, $results) != 1)
8c3bd904 386 {
9133d2c5
DO
387 @ldap_close ($connect);
388 return array ('result' => 'NAK');
d6d79c36 389 }
9133d2c5
DO
390 $info = @ldap_get_entries ($connect, $results);
391 ldap_free_result ($results);
392 $auth_user_name = $info[0]['dn'];
393 }
394 else
3a089a44 395 throw new RackTablesError ('LDAP misconfiguration. Cannon build username for authentication.', RackTablesError::MISCONFIGURED);
a09e3ec7
DO
396 if (array_key_exists ('options', $LDAP_options) and is_array ($LDAP_options['options']))
397 foreach ($LDAP_options['options'] as $opt_code => $opt_value)
398 ldap_set_option ($connect, $opt_code, $opt_value);
9133d2c5
DO
399 $bind = @ldap_bind ($connect, $auth_user_name, $password);
400 if ($bind === FALSE)
401 switch (ldap_errno ($connect))
d6d79c36 402 {
9133d2c5
DO
403 case 49: // LDAP_INVALID_CREDENTIALS
404 return array ('result' => 'NAK');
405 default:
406 return array ('result' => 'CAN');
8c3bd904 407 }
9133d2c5
DO
408 // preliminary decision may change during searching
409 $ret = array ('result' => 'ACK', 'displayed_name' => '', 'memberof' => array());
410 // Some servers deny anonymous search, thus search (if requested) only after binding.
411 // Displayed name only makes sense for authenticated users anyway.
412 if
413 (
59a83bd8
DO
414 isset ($LDAP_options['displayname_attrs']) and
415 strlen ($LDAP_options['displayname_attrs']) and
416 isset ($LDAP_options['search_dn']) and
417 strlen ($LDAP_options['search_dn']) and
418 isset ($LDAP_options['search_attr']) and
419 strlen ($LDAP_options['search_attr'])
9133d2c5
DO
420 )
421 {
422 $results = @ldap_search
423 (
424 $connect,
ef44d4a3
DO
425 $LDAP_options['search_dn'],
426 '(' . $LDAP_options['search_attr'] . "=${username})",
427 array_merge (array ('memberof'), explode (' ', $LDAP_options['displayname_attrs']))
9133d2c5
DO
428 );
429 if (@ldap_count_entries ($connect, $results) != 1)
ae65938e
DO
430 {
431 @ldap_close ($connect);
9133d2c5 432 return array ('result' => 'NAK');
ae65938e 433 }
9133d2c5
DO
434 $info = @ldap_get_entries ($connect, $results);
435 ldap_free_result ($results);
436 $space = '';
ef44d4a3 437 foreach (explode (' ', $LDAP_options['displayname_attrs']) as $attr)
9133d2c5
DO
438 {
439 $ret['displayed_name'] .= $space . $info[0][$attr][0];
440 $space = ' ';
441 }
442 // Pull group membership, if any was returned.
443 if (isset ($info[0]['memberof']))
444 for ($i = 0; $i < $info[0]['memberof']['count']; $i++)
445 foreach (explode (',', $info[0]['memberof'][$i]) as $pair)
446 {
7dd5e6ca
AA
447 $items = explode ('=', $pair);
448 if (count ($items) != 2)
449 continue;
450 list ($attr_name, $attr_value) = $items;
f3e20104 451 if (strtoupper ($attr_name) == 'CN' and validTagName ('$lgcn_' . $attr_value, TRUE))
9133d2c5
DO
452 $ret['memberof'][] = '$lgcn_' . $attr_value;
453 }
8c3bd904 454 }
ae65938e 455 @ldap_close ($connect);
9133d2c5 456 return $ret;
7dfd5e44
DO
457}
458
d16af52f 459function authenticated_via_database ($userinfo, $password)
7dfd5e44 460{
d16af52f 461 if (!isset ($userinfo['user_id'])) // not a local account
b82cce3f 462 return FALSE;
f3371850 463 return $userinfo['user_password_hash'] == sha1 ($password);
e673ee24
DO
464}
465
e673ee24 466?>