r2773 - generateEntityAutoTags(): always accept cell structure on input
[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 $remote_userid == 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 $remote_displayname = $newinfo['displayed_name'];
167 foreach ($newinfo['memberof'] as $autotag)
168 $auto_tags[] = array ('tag' => $autotag);
169 replaceLDAPCacheRecord ($username, sha1 ($password), $newinfo['displayed_name'], $newinfo['memberof']);
170 }
171 releaseLDAPCache();
172 // Do cache maintenence each time fresh data is stored.
173 discardLDAPCache ($LDAP_options['cache_expiry']);
174 return $newinfo['result'] == 'ACK';
175 }
176 // There are two confidence levels of cache hits: "certain" and "uncertain". In either case
177 // expect authentication success, unless it's well-timed to perform a retry,
178 // which may sometimes bring a NAK decision.
179 if ($oldinfo['success_age'] < $LDAP_options['cache_refresh'] or $oldinfo['retry_age'] < $LDAP_options['cache_retry'])
180 {
181 releaseLDAPCache();
182 $remote_displayname = $oldinfo['displayed_name'];
183 foreach ($oldinfo['memberof'] as $autotag)
184 $auto_tags[] = array ('tag' => $autotag);
185 return TRUE;
186 }
187 // Either refresh threshold or retry threshold reached.
188 $newinfo = queryLDAPServer ($username, $password);
189 switch ($newinfo['result'])
190 {
191 case 'ACK': // refresh existing record
192 $remote_displayname = $newinfo['displayed_name'];
193 foreach ($newinfo['memberof'] as $autotag)
194 $auto_tags[] = array ('tag' => $autotag);
195 replaceLDAPCacheRecord ($username, sha1 ($password), $newinfo['displayed_name'], $newinfo['memberof']);
196 releaseLDAPCache();
197 return TRUE;
198 case 'NAK': // The record isn't valid any more.
199 deleteLDAPCacheRecord ($username);
200 releaseLDAPCache();
201 return FALSE;
202 case 'CAN': // retry failed, do nothing, use old value till next retry
203 $remote_displayname = $oldinfo['displayed_name'];
204 foreach ($oldinfo['memberof'] as $autotag)
205 $auto_tags[] = array ('tag' => $autotag);
206 touchLDAPCacheRecord ($username);
207 releaseLDAPCache();
208 return TRUE;
209 default:
210 showError ('Internal error during LDAP cache dispatching', __FUNCTION__);
211 die;
212 }
213 // This is never reached.
214 return FALSE;
215 }
216
217 // Attempt a server conversation and return an array describing the outcome:
218 //
219 // 'result' => 'CAN' : connect (or search) failed completely
220 //
221 // 'result' => 'NAK' : server replied and denied access (or search returned odd data)
222 //
223 // 'result' => 'ACK' : server replied and cleared access, there were no search errors
224 // 'displayed_name' : a string built according to LDAP displayname_attrs option
225 // 'memberof' => filtered list of all LDAP groups the user belongs to
226 //
227 function queryLDAPServer ($username, $password)
228 {
229 global $LDAP_options;
230
231 $connect = @ldap_connect ($LDAP_options['server']);
232 if ($connect === FALSE)
233 return array ('result' => 'CAN');
234
235 // Decide on the username we will actually authenticate for.
236 if (isset ($LDAP_options['domain']) and !empty ($LDAP_options['domain']))
237 $auth_user_name = $username . "@" . $LDAP_options['domain'];
238 elseif
239 (
240 isset ($LDAP_options['search_dn']) and
241 !empty ($LDAP_options['search_dn']) and
242 isset ($LDAP_options['search_attr']) and
243 !empty ($LDAP_options['search_attr'])
244 )
245 {
246 $results = @ldap_search ($connect, $LDAP_options['search_dn'], '(' . $LDAP_options['search_attr'] . "=${username})", array("dn"));
247 if ($results === FALSE)
248 return array ('result' => 'CAN');
249 if (@ldap_count_entries ($connect, $results) != 1)
250 {
251 @ldap_close ($connect);
252 return array ('result' => 'NAK');
253 }
254 $info = @ldap_get_entries ($connect, $results);
255 ldap_free_result ($results);
256 $auth_user_name = $info[0]['dn'];
257 }
258 else
259 {
260 showError ('LDAP misconfiguration. Cannon build username for authentication.', __FUNCTION__);
261 die;
262 }
263 $bind = @ldap_bind ($connect, $auth_user_name, $password);
264 if ($bind === FALSE)
265 switch (ldap_errno ($connect))
266 {
267 case 49: // LDAP_INVALID_CREDENTIALS
268 return array ('result' => 'NAK');
269 default:
270 return array ('result' => 'CAN');
271 }
272 // preliminary decision may change during searching
273 $ret = array ('result' => 'ACK', 'displayed_name' => '', 'memberof' => array());
274 // Some servers deny anonymous search, thus search (if requested) only after binding.
275 // Displayed name only makes sense for authenticated users anyway.
276 if
277 (
278 isset ($LDAP_options['displayname_attrs']) and
279 count ($LDAP_options['displayname_attrs']) and
280 isset ($LDAP_options['search_dn']) and
281 !empty ($LDAP_options['search_dn']) and
282 isset ($LDAP_options['search_attr']) and
283 !empty ($LDAP_options['search_attr'])
284 )
285 {
286 $results = @ldap_search
287 (
288 $connect,
289 $LDAP_options['search_dn'],
290 '(' . $LDAP_options['search_attr'] . "=${username})",
291 array_merge (array ('memberof'), explode (' ', $LDAP_options['displayname_attrs']))
292 );
293 if (@ldap_count_entries ($connect, $results) != 1)
294 {
295 @ldap_close ($connect);
296 return array ('result' => 'NAK');
297 }
298 $info = @ldap_get_entries ($connect, $results);
299 ldap_free_result ($results);
300 $space = '';
301 foreach (explode (' ', $LDAP_options['displayname_attrs']) as $attr)
302 {
303 $ret['displayed_name'] .= $space . $info[0][$attr][0];
304 $space = ' ';
305 }
306 // Pull group membership, if any was returned.
307 if (isset ($info[0]['memberof']))
308 for ($i = 0; $i < $info[0]['memberof']['count']; $i++)
309 foreach (explode (',', $info[0]['memberof'][$i]) as $pair)
310 {
311 list ($attr_name, $attr_value) = explode ('=', $pair);
312 if ($attr_name == 'CN' and validTagName ('$lgcn_' . $attr_value, TRUE))
313 $ret['memberof'][] = '$lgcn_' . $attr_value;
314 }
315 }
316 @ldap_close ($connect);
317 return $ret;
318 }
319
320 function authenticated_via_database ($userinfo, $password)
321 {
322 if (!isset ($userinfo['user_id'])) // not a local account
323 return FALSE;
324 return $userinfo['user_password_hash'] == sha1 ($password);
325 }
326
327 ?>