r2773 - generateEntityAutoTags(): always accept cell structure on input
[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
b82cce3f 11// anonymous binding). It also initializes $remote_* and $*_tags vars.
204284ba 12// Fatal errors are followed by exit (1) to aid in script debugging.
e673ee24
DO
13function authenticate ()
14{
b82cce3f
DO
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))
204284ba 23 {
b82cce3f 24 showError ('secret.php misconfiguration: either user_auth_src or require_local_account are missing', __FUNCTION__);
204284ba
DO
25 exit (1);
26 }
dc9ea133 27 if (isset ($_REQUEST['logout']))
204284ba
DO
28 dieWith401(); // Reset browser credentials cache.
29 switch ($user_auth_src)
e673ee24 30 {
dc9ea133
DO
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;
e673ee24 58 }
d16af52f
DO
59 $userinfo = constructUserCell ($remote_username);
60 if ($require_local_account and !isset ($userinfo['user_id']))
b82cce3f 61 dieWith401();
d16af52f
DO
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']);
dc9ea133
DO
65 switch (TRUE)
66 {
67 // Just trust the server, because the password isn't known.
204284ba 68 case ('httpd' == $user_auth_src):
b82cce3f 69 return;
dc9ea133 70 // When using LDAP, leave a mean to fix things. Admin user is always authenticated locally.
b82cce3f 71 case ('database' == $user_auth_src or $remote_userid == 1):
d16af52f 72 if (authenticated_via_database ($userinfo, $_SERVER['PHP_AUTH_PW']))
dc9ea133
DO
73 return;
74 break;
204284ba 75 case ('ldap' == $user_auth_src):
dc9ea133
DO
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
86function 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();
e673ee24
DO
92}
93
da958e52
DO
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.
46f92ff7 97function permitted ($p = NULL, $t = NULL, $o = NULL, $annex = array())
e673ee24 98{
da958e52 99 global $pageno, $tabno, $op;
7ddb2c05 100 global $auto_tags;
da958e52
DO
101
102 if ($p === NULL)
103 $p = $pageno;
104 if ($t === NULL)
105 $t = $tabno;
00f887f4
DO
106 $my_auto_tags = $auto_tags;
107 $my_auto_tags[] = array ('tag' => '$page_' . $p);
108 $my_auto_tags[] = array ('tag' => '$tab_' . $t);
54ecad0c 109 if ($o === NULL and !empty ($op)) // $op can be set to empty string
00f887f4
DO
110 {
111 $my_auto_tags[] = array ('tag' => '$op_' . $op);
112 $my_auto_tags[] = array ('tag' => '$any_op');
113 }
da958e52
DO
114 $subject = array_merge
115 (
00f887f4 116 $my_auto_tags,
46f92ff7 117 $annex
da958e52 118 );
1c9621a7
DO
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".
da958e52 144 return gotClearanceForTagChain ($subject);
e673ee24
DO
145}
146
7dfd5e44 147function authenticated_via_ldap ($username, $password)
9133d2c5 148{
ef44d4a3 149 global $LDAP_options, $remote_displayname, $auto_tags;
9133d2c5 150
ef44d4a3
DO
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']);
9133d2c5
DO
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();
f3371850
DO
172 // Do cache maintenence each time fresh data is stored.
173 discardLDAPCache ($LDAP_options['cache_expiry']);
9133d2c5
DO
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.
ef44d4a3 179 if ($oldinfo['success_age'] < $LDAP_options['cache_refresh'] or $oldinfo['retry_age'] < $LDAP_options['cache_retry'])
9133d2c5
DO
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
ef44d4a3 224// 'displayed_name' : a string built according to LDAP displayname_attrs option
9133d2c5
DO
225// 'memberof' => filtered list of all LDAP groups the user belongs to
226//
227function queryLDAPServer ($username, $password)
7dfd5e44 228{
ef44d4a3 229 global $LDAP_options;
9133d2c5 230
ef44d4a3 231 $connect = @ldap_connect ($LDAP_options['server']);
9133d2c5
DO
232 if ($connect === FALSE)
233 return array ('result' => 'CAN');
234
235 // Decide on the username we will actually authenticate for.
ef44d4a3
DO
236 if (isset ($LDAP_options['domain']) and !empty ($LDAP_options['domain']))
237 $auth_user_name = $username . "@" . $LDAP_options['domain'];
9133d2c5
DO
238 elseif
239 (
ef44d4a3
DO
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'])
9133d2c5 244 )
8c3bd904 245 {
ef44d4a3 246 $results = @ldap_search ($connect, $LDAP_options['search_dn'], '(' . $LDAP_options['search_attr'] . "=${username})", array("dn"));
9133d2c5
DO
247 if ($results === FALSE)
248 return array ('result' => 'CAN');
249 if (@ldap_count_entries ($connect, $results) != 1)
8c3bd904 250 {
9133d2c5
DO
251 @ldap_close ($connect);
252 return array ('result' => 'NAK');
d6d79c36 253 }
9133d2c5
DO
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))
d6d79c36 266 {
9133d2c5
DO
267 case 49: // LDAP_INVALID_CREDENTIALS
268 return array ('result' => 'NAK');
269 default:
270 return array ('result' => 'CAN');
8c3bd904 271 }
9133d2c5
DO
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 (
ef44d4a3
DO
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'])
9133d2c5
DO
284 )
285 {
286 $results = @ldap_search
287 (
288 $connect,
ef44d4a3
DO
289 $LDAP_options['search_dn'],
290 '(' . $LDAP_options['search_attr'] . "=${username})",
291 array_merge (array ('memberof'), explode (' ', $LDAP_options['displayname_attrs']))
9133d2c5
DO
292 );
293 if (@ldap_count_entries ($connect, $results) != 1)
ae65938e
DO
294 {
295 @ldap_close ($connect);
9133d2c5 296 return array ('result' => 'NAK');
ae65938e 297 }
9133d2c5
DO
298 $info = @ldap_get_entries ($connect, $results);
299 ldap_free_result ($results);
300 $space = '';
ef44d4a3 301 foreach (explode (' ', $LDAP_options['displayname_attrs']) as $attr)
9133d2c5
DO
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 }
8c3bd904 315 }
ae65938e 316 @ldap_close ($connect);
9133d2c5 317 return $ret;
7dfd5e44
DO
318}
319
d16af52f 320function authenticated_via_database ($userinfo, $password)
7dfd5e44 321{
d16af52f 322 if (!isset ($userinfo['user_id'])) // not a local account
b82cce3f 323 return FALSE;
f3371850 324 return $userinfo['user_password_hash'] == sha1 ($password);
e673ee24
DO
325}
326
e673ee24 327?>