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