root/fAuthorization.php

Revision 766, 14.1 kB (checked in by wbond, 4 days ago)

Fixed ticket #363 - added fAuthorization::getLoginPage()

LineHide Line Numbers
1 <?php
2 /**
3  * Allows defining and checking user authentication via ACLs, authorization levels or a simple logged in/not logged in scheme
4  *
5  * @copyright  Copyright (c) 2007-2010 Will Bond
6  * @author     Will Bond [wb] <will@flourishlib.com>
7  * @license    http://flourishlib.com/license
8  *
9  * @package    Flourish
10  * @link       http://flourishlib.com/fAuthorization
11  *
12  * @version    1.0.0b5
13  * @changes    1.0.0b5  Added ::getLoginPage() [wb, 2010-03-09]
14  * @changes    1.0.0b4  Updated class to use new fSession API [wb, 2009-10-23]
15  * @changes    1.0.0b3  Updated class to use new fSession API [wb, 2009-05-08]
16  * @changes    1.0.0b2  Fixed a bug with using named IP ranges in ::checkIP() [wb, 2009-01-10]
17  * @changes    1.0.0b   The initial implementation [wb, 2007-06-14]
18  */
19 class fAuthorization
20 {
21     // The following constants allow for nice looking callbacks to static methods
22     const addNamedIPRange  = 'fAuthorization::addNamedIPRange';
23     const checkACL         = 'fAuthorization::checkACL';
24     const checkAuthLevel   = 'fAuthorization::checkAuthLevel';
25     const checkIP          = 'fAuthorization::checkIP';
26     const checkLoggedIn    = 'fAuthorization::checkLoggedIn';
27     const destroyUserInfo  = 'fAuthorization::destroyUserInfo';
28     const getLoginPage     = 'fAuthorization::getLoginPage';
29     const getRequestedURL  = 'fAuthorization::getRequestedURL';
30     const getUserACLs      = 'fAuthorization::getUserACLs';
31     const getUserAuthLevel = 'fAuthorization::getUserAuthLevel';
32     const getUserToken     = 'fAuthorization::getUserToken';
33     const requireACL       = 'fAuthorization::requireACL';
34     const requireAuthLevel = 'fAuthorization::requireAuthLevel';
35     const requireLoggedIn  = 'fAuthorization::requireLoggedIn';
36     const reset            = 'fAuthorization::reset';
37     const setAuthLevels    = 'fAuthorization::setAuthLevels';
38     const setLoginPage     = 'fAuthorization::setLoginPage';
39     const setRequestedURL  = 'fAuthorization::setRequestedURL';
40     const setUserACLs      = 'fAuthorization::setUserACLs';
41     const setUserAuthLevel = 'fAuthorization::setUserAuthLevel';
42     const setUserToken     = 'fAuthorization::setUserToken';
43    
44    
45     /**
46     * The valid auth levels
47     *
48     * @var array
49     */
50     static private $levels = NULL;
51    
52     /**
53     * The login page
54     *
55     * @var string
56     */
57     static private $login_page = NULL;
58    
59     /**
60     * Named IP ranges
61     *
62     * @var array
63     */
64     static private $named_ip_ranges = array();
65    
66    
67     /**
68     * Adds a named IP address or range, or array of addresses and/or ranges
69     *
70     * This method allows ::checkIP() to be called with a name instead of the
71     * actual IPs.
72     *
73     * @param  string $name       The name to give the IP addresses/ranges
74     * @param  mixed  $ip_ranges  This can be string (or array of strings) of the IPs or IP ranges to restrict to - please see ::checkIP() for format details
75     * @return void
76     */
77     static public function addNamedIPRange($name, $ip_ranges)
78     {
79         self::$named_ip_ranges[$name] = $ip_ranges;
80     }
81    
82    
83     /**
84     * Checks to see if the logged in user meets the requirements of the ACL specified
85     *
86     * @param  string $resource    The resource we are checking permissions for
87     * @param  string $permission  The permission to require from the user
88     * @return boolean  If the user has the required permissions
89     */
90     static public function checkACL($resource, $permission)
91     {
92         if (self::getUserACLs() === NULL) {
93             return FALSE;
94         }
95            
96         $acls = self::getUserACLs();
97        
98         if (!isset($acls[$resource]) && !isset($acls['*'])) {
99             return FALSE;
100         }
101        
102         if (isset($acls[$resource])) {
103             if (in_array($permission, $acls[$resource]) || in_array('*', $acls[$resource])) {
104                 return TRUE;
105             }
106         }
107        
108         if (isset($acls['*'])) {
109             if (in_array($permission, $acls['*']) || in_array('*', $acls['*'])) {
110                 return TRUE;
111             }
112         }
113        
114         return FALSE;
115     }
116    
117    
118     /**
119     * Checks to see if the logged in user has the specified auth level
120     *
121     * @param  string $level  The level to check against the logged in user's level
122     * @return boolean  If the user has the required auth level
123     */
124     static public function checkAuthLevel($level)
125     {
126         if (self::getUserAuthLevel()) {
127            
128             self::validateAuthLevel(self::getUserAuthLevel());
129             self::validateAuthLevel($level);
130            
131             $user_number = self::$levels[self::getUserAuthLevel()];
132             $required_number = self::$levels[$level];
133            
134             if ($user_number >= $required_number) {
135                 return TRUE;
136             }
137         }
138        
139         return FALSE;
140     }
141    
142    
143     /**
144     * Checks to see if the user is from the IPs or IP ranges specified
145     *
146     * The `$ip_ranges` parameter can be either a single string, or an array of
147     * strings, each of which should be in one of the following formats:
148    
149     *  - A single IP address:
150     *   - 192.168.1.1
151     *   - 208.77.188.166
152     *  - A CIDR range
153     *   - 192.168.1.0/24
154     *   - 208.77.188.160/28
155     *  - An IP/subnet mask combination
156     *   - 192.168.1.0/255.255.255.0
157     *   - 208.77.188.160/255.255.255.240
158     *
159     * @param  mixed $ip_ranges  A string (or array of strings) of the IPs or IP ranges to restrict to - see method description for details
160     * @return boolean  If the user is coming from (one of) the IPs or ranges specified
161     */
162     static public function checkIP($ip_ranges)
163     {
164         // Check to see if a named IP range was specified
165         if (is_string($ip_ranges) && isset(self::$named_ip_ranges[$ip_ranges])) {
166             $ip_ranges = self::$named_ip_ranges[$ip_ranges];
167         }
168        
169         // Get the remote IP and remove any IPv6 to IPv4 mapping
170         $user_ip      = str_replace('::ffff:', '', $_SERVER['REMOTE_ADDR']);
171         $user_ip_long = ip2long($user_ip);
172        
173         settype($ip_ranges, 'array');
174        
175         foreach ($ip_ranges as $ip_range) {
176            
177             if (strpos($ip_range, '/') === FALSE) {
178                 $ip_range .= '/32';
179             }
180            
181             list($range_ip, $range_mask) = explode('/', $ip_range);
182            
183             if (strlen($range_mask) < 3) {
184                 $mask_long = pow(2, 32) - pow(2, 32 - $range_mask);
185             } else {
186                 $mask_long = ip2long($range_mask);
187             }
188            
189             $range_ip_long = ip2long($range_ip);
190            
191             if (($range_ip_long & $mask_long) != $range_ip_long) {
192                 $proper_range_ip = long2ip($range_ip_long & $mask_long);
193                 throw new fProgrammerException(
194                     'The range base IP address specified, %1$s, is invalid for the CIDR range or subnet mask provided (%2$s). The proper IP is %3$s.',
195                     $range_ip,
196                     '/' . $range_mask,
197                     $proper_range_ip
198                 );
199             }
200            
201             if (($user_ip_long & $mask_long) == $range_ip_long) {
202                 return TRUE;
203             }
204         }
205        
206         return FALSE;
207     }
208    
209    
210     /**
211     * Checks to see if the user has an auth level or ACLs defined
212     *
213     * @return boolean  If the user is logged in
214     */
215     static public function checkLoggedIn()
216     {
217         if (fSession::get(__CLASS__ . '::user_auth_level', NULL) !== NULL ||
218             fSession::get(__CLASS__ . '::user_acls', NULL) !== NULL ||
219             fSession::get(__CLASS__ . '::user_token', NULL) !== NULL) {
220             return TRUE;
221         }
222         return FALSE;
223     }
224    
225    
226     /**
227     * Destroys the user's auth level and/or ACLs
228     *
229     * @return void
230     */
231     static public function destroyUserInfo()
232     {
233         fSession::delete(__CLASS__ . '::user_auth_level');
234         fSession::delete(__CLASS__ . '::user_acls');
235         fSession::delete(__CLASS__ . '::user_token');
236         fSession::delete(__CLASS__ . '::requested_url');
237     }
238    
239    
240     /**
241     * Returns the login page set via ::setLoginPage()
242     *
243     * @return string  The login page users are redirected to if they don't have the required authorization
244     */
245     static public function getLoginPage()
246     {
247         return self::$login_page;
248     }
249    
250     /**
251     * Returns the URL requested before the user was redirected to the login page
252     *
253     * @param  boolean $clear        If the requested url should be cleared from the session after it is retrieved
254     * @param  string  $default_url  The default URL to return if the user was not redirected
255     * @return string  The URL that was requested before they were redirected to the login page
256     */
257     static public function getRequestedURL($clear, $default_url=NULL)
258     {
259         $requested_url = fSession::get(__CLASS__ . '::requested_url', $default_url);
260         if ($clear) {
261             fSession::delete(__CLASS__ . '::requested_url');
262         }
263         return $requested_url;
264     }
265    
266    
267     /**
268     * Gets the ACLs for the logged in user
269     *
270     * @return array  The logged in user's ACLs
271     */
272     static public function getUserACLs()
273     {
274         return fSession::get(__CLASS__ . '::user_acls', NULL);
275     }
276    
277    
278     /**
279     * Gets the authorization level for the logged in user
280     *
281     * @return string  The logged in user's auth level
282     */
283     static public function getUserAuthLevel()
284     {
285         return fSession::get(__CLASS__ . '::user_auth_level', NULL);
286     }
287    
288    
289     /**
290     * Gets the value that was set as the user token, `NULL` if no token has been set
291     *
292     * @return mixed  The user token that had been set, `NULL` if none
293     */
294     static public function getUserToken()
295     {
296         return fSession::get(__CLASS__ . '::user_token', NULL);
297     }
298    
299    
300     /**
301     * Redirects the user to the login page
302     *
303     * @return void
304     */
305     static private function redirect()
306     {
307         self::setRequestedURL(fURL::getWithQueryString());
308         fURL::redirect(self::$login_page);
309     }
310    
311    
312     /**
313     * Redirect the user to the login page if they do not have the permissions required
314     *
315     * @param  string $resource    The resource we are checking permissions for
316     * @param  string $permission  The permission to require from the user
317     * @return void
318     */
319     static public function requireACL($resource, $permission)
320     {
321         self::validateLoginPage();
322        
323         if (self::checkACL($resource, $permission)) {
324             return;
325         }
326        
327         self::redirect();
328     }
329    
330    
331     /**
332     * Redirect the user to the login page if they do not have the auth level required
333     *
334     * @param  string $level  The level to check against the logged in user's level
335     * @return void
336     */
337     static public function requireAuthLevel($level)
338     {
339         self::validateLoginPage();
340        
341         if (self::checkAuthLevel($level)) {
342             return;
343         }
344        
345         self::redirect();
346     }
347    
348    
349     /**
350     * Redirect the user to the login page if they do not have an auth level or ACLs
351     *
352     * @return void
353     */
354     static public function requireLoggedIn()
355     {
356         self::validateLoginPage();
357        
358         if (self::checkLoggedIn()) {
359             return;
360         }
361        
362         self::redirect();
363     }
364    
365    
366     /**
367     * Resets the configuration of the class
368     *
369     * @internal
370      *
371     * @return void
372     */
373     static public function reset()
374     {
375         self::$level           = NULL;
376         self::$login_page      = NULL;
377         self::$named_ip_ranges = array();
378     }
379    
380    
381     /**
382     * Sets the authorization levels to use for level checking
383     *
384     * @param  array $levels  An associative array of `(string) {level} => (integer) {value}`, for each level
385     * @return void
386     */
387     static public function setAuthLevels($levels)
388     {
389         self::$levels = $levels;
390     }
391    
392    
393     /**
394     * Sets the login page to redirect users to
395     *
396     * @param  string $url  The URL of the login page
397     * @return void
398     */
399     static public function setLoginPage($url)
400     {
401         self::$login_page = $url;
402     }
403    
404    
405     /**
406     * Sets the restricted URL requested by the user
407     *
408     * @param  string  $url  The URL to save as the requested URL
409     * @return void
410     */
411     static public function setRequestedURL($url)
412     {
413         fSession::set(__CLASS__ . '::requested_url', $url);
414     }
415    
416    
417     /**
418     * Sets the ACLs for the logged in user.
419     *
420     * Array should be formatted like:
421     *
422     * {{{
423     * array (
424     *     (string) {resource name} => array(
425     *         (mixed) {permission}, ...
426     *     ), ...
427     * )
428     * }}}
429     *
430     * The resource name or the permission may be the single character `'*'`
431     * which acts as a wildcard.
432     *
433     * @param  array $acls  The logged in user's ACLs - see method description for format
434     * @return void
435     */
436     static public function setUserACLs($acls)
437     {
438         fSession::set(__CLASS__ . '::user_acls', $acls);
439         fSession::regenerateID();
440     }
441    
442    
443     /**
444     * Sets the authorization level for the logged in user
445     *
446     * @param  string $level  The logged in user's auth level
447     * @return void
448     */
449     static public function setUserAuthLevel($level)
450     {
451         self::validateAuthLevel($level);
452         fSession::set(__CLASS__ . '::user_auth_level', $level);
453         fSession::regenerateID();
454     }
455    
456    
457     /**
458     * Sets some piece of information to use to identify the current user
459     *
460     * @param  mixed $token  The user's token. This could be a user id, an email address, a user object, etc.
461     * @return void
462     */
463     static public function setUserToken($token)
464     {
465         fSession::set(__CLASS__ . '::user_token', $token);
466         fSession::regenerateID();
467     }
468    
469    
470     /**
471     * Makes sure auth levels have been set, and that the specified auth level is valid
472     *
473     * @param  string $level  The level to validate
474     * @return void
475     */
476     static private function validateAuthLevel($level=NULL)
477     {
478         if (self::$levels === NULL) {
479             throw new fProgrammerException(
480                 'No authorization levels have been set, please call %s',
481                 __CLASS__ . '::setAuthLevels()'
482             );
483         }
484         if ($level !== NULL && !isset(self::$levels[$level])) {
485             throw new fProgrammerException(
486                 'The authorization level specified, %1$s, is invalid. Must be one of: %2$s.',
487                 $level,
488                 join(', ', array_keys(self::$levels))
489             );
490         }
491     }
492    
493    
494     /**
495     * Makes sure a login page has been defined
496     *
497     * @return void
498     */
499     static private function validateLoginPage()
500     {
501         if (self::$login_page === NULL) {
502             throw new fProgrammerException(
503                 'No login page has been set, please call %s',
504                 __CLASS__ . '::setLoginPage()'
505             );
506         }
507     }
508    
509    
510     /**
511     * Forces use as a static class
512     *
513     * @return fAuthorization
514     */
515     private function __construct() { }
516 }
517  
518  
519  
520 /**
521  * Copyright (c) 2007-2010 Will Bond <will@flourishlib.com>
522  *
523  * Permission is hereby granted, free of charge, to any person obtaining a copy
524  * of this software and associated documentation files (the "Software"), to deal
525  * in the Software without restriction, including without limitation the rights
526  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
527  * copies of the Software, and to permit persons to whom the Software is
528  * furnished to do so, subject to the following conditions:
529  *
530  * The above copyright notice and this permission notice shall be included in
531  * all copies or substantial portions of the Software.
532  *
533  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
534  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
535  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
536  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
537  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
538  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
539  * THE SOFTWARE.
540  */