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