r2666 - introduced LDAPCache table (ticket:193)
[racktables] / inc / auth.php
CommitLineData
b325120a 1<?php
e673ee24
DO
2/*
3
4Authentication library for RackTables.
5
6*/
7
8// This function ensures that we don't continue without a legitimate
d4d873be
DO
9// username and password (also make sure, that both are present, this
10// is especially useful for LDAP auth code to not deceive itself with
204284ba
DO
11// anonymous binding). It also initializes $remote_username and $accounts.
12// Fatal errors are followed by exit (1) to aid in script debugging.
e673ee24
DO
13function authenticate ()
14{
d6d79c36 15 global $remote_username, $remote_displayname, $accounts, $user_auth_src, $require_valid_user, $script_mode;
204284ba
DO
16 if (!isset ($user_auth_src) or !isset ($require_valid_user))
17 {
18 showError ('secret.php misconfiguration: either user_auth_src or require_valid_user are missing', __FUNCTION__);
19 exit (1);
20 }
573214e0
DO
21 // This reindexing is necessary after switching to listCells(), which
22 // returns list indexed by id (while many other functions expect the
23 // user list to be indexed by username).
24 if (NULL === ($tmplist = listCells ('user')))
204284ba
DO
25 {
26 showError ('Failed to initialize access database.', __FUNCTION__);
27 exit (1);
28 }
573214e0
DO
29 $accounts = array();
30 foreach ($tmplist as $tmpval)
31 $accounts[$tmpval['user_name']] = $tmpval;
204284ba
DO
32 if (isset ($script_mode) and $script_mode === TRUE)
33 return;
dc9ea133 34 if (isset ($_REQUEST['logout']))
204284ba
DO
35 dieWith401(); // Reset browser credentials cache.
36 switch ($user_auth_src)
e673ee24 37 {
dc9ea133
DO
38 case 'database':
39 case 'ldap':
40 if
41 (
42 !isset ($_SERVER['PHP_AUTH_USER']) or
43 !strlen ($_SERVER['PHP_AUTH_USER']) or
44 !isset ($_SERVER['PHP_AUTH_PW']) or
45 !strlen ($_SERVER['PHP_AUTH_PW'])
46 )
47 dieWith401();
48 $remote_username = $_SERVER['PHP_AUTH_USER'];
49 break;
50 case 'httpd':
51 if
52 (
53 !isset ($_SERVER['REMOTE_USER']) or
54 !strlen ($_SERVER['REMOTE_USER'])
55 )
56 {
57 showError ('System misconfiguration. The web-server didn\'t authenticate the user, although ought to do.');
58 die;
59 }
60 $remote_username = $_SERVER['REMOTE_USER'];
61 break;
62 default:
63 showError ('Invalid authentication source!', __FUNCTION__);
64 die;
e673ee24 65 }
dc9ea133
DO
66 if ($require_valid_user and !isset ($accounts[$remote_username]))
67 dieWith401();
d6d79c36 68 $remote_displayname = $remote_username;
dc9ea133
DO
69 switch (TRUE)
70 {
71 // Just trust the server, because the password isn't known.
204284ba 72 case ('httpd' == $user_auth_src):
dc9ea133
DO
73 if (authenticated_via_httpd ($remote_username))
74 return;
75 break;
76 // When using LDAP, leave a mean to fix things. Admin user is always authenticated locally.
204284ba 77 case ('database' == $user_auth_src or $accounts[$remote_username]['user_id'] == 1):
dc9ea133 78 if (authenticated_via_database ($remote_username, $_SERVER['PHP_AUTH_PW']))
d6d79c36
DO
79 {
80 if (!empty ($accounts[$remote_username]['user_realname']))
81 $remote_displayname = $accounts[$remote_username]['user_realname'];
dc9ea133 82 return;
d6d79c36 83 }
dc9ea133 84 break;
204284ba 85 case ('ldap' == $user_auth_src):
d6d79c36 86 // Call below also sets $remote_displayname.
dc9ea133 87 if (authenticated_via_ldap ($remote_username, $_SERVER['PHP_AUTH_PW']))
d6d79c36
DO
88 {
89 if (!empty ($accounts[$remote_username]['user_realname']))
90 $remote_displayname = $accounts[$remote_username]['user_realname'];
dc9ea133 91 return;
d6d79c36 92 }
dc9ea133
DO
93 break;
94 default:
95 showError ('Invalid authentication source!', __FUNCTION__);
96 die;
97 }
98 dieWith401();
99}
100
101function dieWith401 ()
102{
103 header ('WWW-Authenticate: Basic realm="' . getConfigVar ('enterprise') . ' RackTables access"');
104 header ('HTTP/1.0 401 Unauthorized');
105 showError ('This system requires authentication. You should use a username and a password.');
106 die();
e673ee24
DO
107}
108
da958e52
DO
109// Merge accumulated tags into a single chain, add location-specific
110// autotags and try getting access clearance. Page and tab are mandatory,
111// operation is optional.
46f92ff7 112function permitted ($p = NULL, $t = NULL, $o = NULL, $annex = array())
e673ee24 113{
da958e52 114 global $pageno, $tabno, $op;
7ddb2c05 115 global $auto_tags;
da958e52
DO
116
117 if ($p === NULL)
118 $p = $pageno;
119 if ($t === NULL)
120 $t = $tabno;
00f887f4
DO
121 $my_auto_tags = $auto_tags;
122 $my_auto_tags[] = array ('tag' => '$page_' . $p);
123 $my_auto_tags[] = array ('tag' => '$tab_' . $t);
54ecad0c 124 if ($o === NULL and !empty ($op)) // $op can be set to empty string
00f887f4
DO
125 {
126 $my_auto_tags[] = array ('tag' => '$op_' . $op);
127 $my_auto_tags[] = array ('tag' => '$any_op');
128 }
da958e52
DO
129 $subject = array_merge
130 (
00f887f4 131 $my_auto_tags,
46f92ff7 132 $annex
da958e52 133 );
1c9621a7
DO
134 // XXX: The solution below is only appropriate for a corner case of a more universal
135 // problem: to make the decision for an entity belonging to a cascade of nested
136 // containers. Each container being an entity itself, it may have own tags (explicit
137 // and implicit accordingly). There's a fixed set of rules (RackCode) with each rule
138 // being able to evaluate any built and given context and produce either a decision
139 // or a lack of decision.
140 // There are several levels of context for the target entity, at least one for entities
141 // belonging directly to the tree root. Each level's context is a union of given
142 // container's tags and the tags of the contained entities.
143 // The universal problem originates from the fact, that certain rules may change
144 // their product as context level changes, thus forcing some final decision (but not
145 // adding a lack of it). With rule code being principles and context cascade being
146 // circumstances, there are two uttermost approaches or moralities.
147 //
148 // Fundamentalism: principles over circumstances. When a rule doesn't produce any
149 // decision, go on to the next rule. When all rules are evaluated, go on to the next
150 // security context level.
151 //
152 // Opportunism: circumstances over principles. With a lack of decision, work with the
153 // same rule, trying to evaluate it against the next level (and next, and next...),
154 // until all levels are tried. Only then go on to the next rule.
155 //
156 // With the above being simple discrete algorythms, I believe, that they very reliably
157 // replicate human behavior. This gives a vast ground for further research, so I would
158 // only note, that the morale used in RackTables is "principles first".
da958e52 159 return gotClearanceForTagChain ($subject);
e673ee24
DO
160}
161
7dfd5e44 162function authenticated_via_ldap ($username, $password)
9133d2c5
DO
163{
164 global
165 $ldap_cache_refresh, // read
166 $ldap_cache_retry, // read
167 $ldap_cache_expiry, // read
168 $remote_displayname, // set
169 $auto_tags; // set
170
171 $oldinfo = acquireLDAPCache ($username, sha1 ($password), $ldap_cache_expiry);
172 // Remember to have releaseLDAPCache() called before any return statement.
173 if ($oldinfo === NULL) // cache miss
174 {
175 // On cache miss execute complete procedure and return the result. In case
176 // of successful authentication put a record into cache.
177 $newinfo = queryLDAPServer ($username, $password);
178 if ($newinfo['result'] == 'ACK')
179 {
180 $remote_displayname = $newinfo['displayed_name'];
181 foreach ($newinfo['memberof'] as $autotag)
182 $auto_tags[] = array ('tag' => $autotag);
183 replaceLDAPCacheRecord ($username, sha1 ($password), $newinfo['displayed_name'], $newinfo['memberof']);
184 }
185 releaseLDAPCache();
186 return $newinfo['result'] == 'ACK';
187 }
188 // There are two confidence levels of cache hits: "certain" and "uncertain". In either case
189 // expect authentication success, unless it's well-timed to perform a retry,
190 // which may sometimes bring a NAK decision.
191 if ($oldinfo['success_age'] < $ldap_cache_refresh or $oldinfo['retry_age'] < $ldap_cache_retry)
192 {
193 releaseLDAPCache();
194 $remote_displayname = $oldinfo['displayed_name'];
195 foreach ($oldinfo['memberof'] as $autotag)
196 $auto_tags[] = array ('tag' => $autotag);
197 return TRUE;
198 }
199 // Either refresh threshold or retry threshold reached.
200 $newinfo = queryLDAPServer ($username, $password);
201 switch ($newinfo['result'])
202 {
203 case 'ACK': // refresh existing record
204 $remote_displayname = $newinfo['displayed_name'];
205 foreach ($newinfo['memberof'] as $autotag)
206 $auto_tags[] = array ('tag' => $autotag);
207 replaceLDAPCacheRecord ($username, sha1 ($password), $newinfo['displayed_name'], $newinfo['memberof']);
208 releaseLDAPCache();
209 return TRUE;
210 case 'NAK': // The record isn't valid any more.
211 deleteLDAPCacheRecord ($username);
212 releaseLDAPCache();
213 return FALSE;
214 case 'CAN': // retry failed, do nothing, use old value till next retry
215 $remote_displayname = $oldinfo['displayed_name'];
216 foreach ($oldinfo['memberof'] as $autotag)
217 $auto_tags[] = array ('tag' => $autotag);
218 touchLDAPCacheRecord ($username);
219 releaseLDAPCache();
220 return TRUE;
221 default:
222 showError ('Internal error during LDAP cache dispatching', __FUNCTION__);
223 die;
224 }
225 // This is never reached.
226 return FALSE;
227}
228
229// Attempt a server conversation and return an array describing the outcome:
230//
231// 'result' => 'CAN' : connect (or search) failed completely
232//
233// 'result' => 'NAK' : server replied and denied access (or search returned odd data)
234//
235// 'result' => 'ACK' : server replied and cleared access, there were no search errors
236// 'displayed_name' : a string built according to ldap_displayname_attrs option
237// 'memberof' => filtered list of all LDAP groups the user belongs to
238//
239function queryLDAPServer ($username, $password)
7dfd5e44 240{
8c3bd904 241 global $ldap_server, $ldap_domain, $ldap_search_dn, $ldap_search_attr;
9133d2c5
DO
242 global
243 $ldap_server,
244 $ldap_domain,
245 $ldap_search_dn,
246 $ldap_search_attr,
247 $ldap_displayname_attrs;
248
249 $connect = @ldap_connect ($ldap_server);
250 if ($connect === FALSE)
251 return array ('result' => 'CAN');
252
253 // Decide on the username we will actually authenticate for.
254 if (isset ($ldap_domain) and !empty ($ldap_domain))
255 $auth_user_name = $username . "@" . $ldap_domain;
256 elseif
257 (
258 isset ($ldap_search_dn) and
259 !empty ($ldap_search_dn) and
260 isset ($ldap_search_attr) and
261 !empty ($ldap_search_attr)
262 )
8c3bd904 263 {
9133d2c5
DO
264 $results = @ldap_search ($connect, $ldap_search_dn, "(${ldap_search_attr}=${username})", array("dn"));
265 if ($results === FALSE)
266 return array ('result' => 'CAN');
267 if (@ldap_count_entries ($connect, $results) != 1)
8c3bd904 268 {
9133d2c5
DO
269 @ldap_close ($connect);
270 return array ('result' => 'NAK');
d6d79c36 271 }
9133d2c5
DO
272 $info = @ldap_get_entries ($connect, $results);
273 ldap_free_result ($results);
274 $auth_user_name = $info[0]['dn'];
275 }
276 else
277 {
278 showError ('LDAP misconfiguration. Cannon build username for authentication.', __FUNCTION__);
279 die;
280 }
281 $bind = @ldap_bind ($connect, $auth_user_name, $password);
282 if ($bind === FALSE)
283 switch (ldap_errno ($connect))
d6d79c36 284 {
9133d2c5
DO
285 case 49: // LDAP_INVALID_CREDENTIALS
286 return array ('result' => 'NAK');
287 default:
288 return array ('result' => 'CAN');
8c3bd904 289 }
9133d2c5
DO
290 // preliminary decision may change during searching
291 $ret = array ('result' => 'ACK', 'displayed_name' => '', 'memberof' => array());
292 // Some servers deny anonymous search, thus search (if requested) only after binding.
293 // Displayed name only makes sense for authenticated users anyway.
294 if
295 (
296 isset ($ldap_displayname_attrs) and
297 count ($ldap_displayname_attrs) and
298 isset ($ldap_search_dn) and
299 !empty ($ldap_search_dn) and
300 isset ($ldap_search_attr) and
301 !empty ($ldap_search_attr)
302 )
303 {
304 $results = @ldap_search
305 (
306 $connect,
307 $ldap_search_dn,
308 "(${ldap_search_attr}=${username})",
309 array_merge (array ('memberof'), $ldap_displayname_attrs)
310 );
311 if (@ldap_count_entries ($connect, $results) != 1)
ae65938e
DO
312 {
313 @ldap_close ($connect);
9133d2c5 314 return array ('result' => 'NAK');
ae65938e 315 }
9133d2c5
DO
316 $info = @ldap_get_entries ($connect, $results);
317 ldap_free_result ($results);
318 $space = '';
319 foreach ($ldap_displayname_attrs as $attr)
320 {
321 $ret['displayed_name'] .= $space . $info[0][$attr][0];
322 $space = ' ';
323 }
324 // Pull group membership, if any was returned.
325 if (isset ($info[0]['memberof']))
326 for ($i = 0; $i < $info[0]['memberof']['count']; $i++)
327 foreach (explode (',', $info[0]['memberof'][$i]) as $pair)
328 {
329 list ($attr_name, $attr_value) = explode ('=', $pair);
330 if ($attr_name == 'CN' and validTagName ('$lgcn_' . $attr_value, TRUE))
331 $ret['memberof'][] = '$lgcn_' . $attr_value;
332 }
8c3bd904 333 }
ae65938e 334 @ldap_close ($connect);
9133d2c5 335 return $ret;
7dfd5e44
DO
336}
337
338function authenticated_via_database ($username, $password)
339{
340 global $accounts;
93bdb7ba 341 if (!function_exists ('sha1'))
7dfd5e44 342 {
93bdb7ba 343 showError ('Fatal error: PHP sha1() function is missing', __FUNCTION__);
7dfd5e44
DO
344 die();
345 }
346 if (!isset ($accounts[$username]['user_password_hash']))
347 return FALSE;
93bdb7ba 348 if ($accounts[$username]['user_password_hash'] == sha1 ($password))
e673ee24
DO
349 return TRUE;
350 return FALSE;
351}
352
dc9ea133
DO
353function authenticated_via_httpd ($username)
354{
355 // Reaching here means, that .htaccess authentication passed.
356 // Let's make sure, that user exists in the database, and give clearance.
357 global $accounts;
358 return isset ($accounts[$username]);
359}
360
e673ee24
DO
361// This function returns password hash for given user ID.
362function getHashByID ($user_id = 0)
363{
364 if ($user_id <= 0)
365 {
b09549b3 366 showError ('Invalid user_id', __FUNCTION__);
e673ee24
DO
367 return NULL;
368 }
369 global $accounts;
370 foreach ($accounts as $account)
371 if ($account['user_id'] == $user_id)
372 return $account['user_password_hash'];
373 return NULL;
374}
375
b9bd9897
DO
376// Likewise.
377function getUsernameByID ($user_id = 0)
378{
379 if ($user_id <= 0)
380 {
381 showError ('Invalid user_id', __FUNCTION__);
382 return NULL;
383 }
384 global $accounts;
385 foreach ($accounts as $account)
386 if ($account['user_id'] == $user_id)
387 return $account['user_name'];
388 showError ("User with ID '${user_id}' not found!");
389 return NULL;
390}
391
e673ee24 392?>