source: trunk/lib/ACL.inc.php @ 173

Last change on this file since 173 was 173, checked in by scdev, 18 years ago

Q - little bugs fixing in ACL and acl.cli.php.

File size: 16.6 KB
Line 
1<?php
2/*
3* ACL.inc.php
4*
5* Uses the ARO/ACO/AXO model of Access Control Lists.
6* Uses Modified Preorder Tree Traversal to maintain a tree-structure.
7* See: http://www.sitepoint.com/print/hierarchical-data-database
8* Includes a command-line tool for managing rights.
9*
10* Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information.
11* @author   Quinn Comendant <quinn@strangecode.com>
12* @version  1.0
13* @since    14 Jun 2006 22:35:11
14*/
15
16require_once dirname(__FILE__) . '/Cache.inc.php';
17
18class ACL {
19
20    // Configuration parameters for this object.
21    var $_params = array(
22       
23        // If false nothing will be cached or retreived. Useful for testing realtime data requests.
24        'enable_cache' => true,
25
26        // Automatically create table and verify columns. Better set to false after site launch.
27        'create_table' => false,
28    );
29
30    /**
31     * Prefs constructor.
32     */
33    function ACL()
34    {
35        $app =& App::getInstance();
36
37        // Configure the cache object.
38        $this->cache = new Cache('acl');
39        $this->cache->setParam(array('enabled' => true));
40
41        // Get create tables config from global context.
42        if (!is_null($app->getParam('db_create_tables'))) {
43            $this->setParam(array('create_table' => $app->getParam('db_create_tables')));
44        }
45    }
46
47    /**
48     * This method enforces the singleton pattern for this class.
49     *
50     * @return  object  Reference to the global ACL object.
51     * @access  public
52     * @static
53     */
54    function &getInstance()
55    {
56        static $instance = null;
57
58        if ($instance === null) {
59            $instance = new ACL();
60        }
61
62        return $instance;
63    }
64
65    /**
66     * Set (or overwrite existing) parameters by passing an array of new parameters.
67     *
68     * @access public
69     *
70     * @param  array    $params     Array of parameters (key => val pairs).
71     */
72    function setParam($params)
73    {
74        $app =& App::getInstance();
75   
76        if (isset($params) && is_array($params)) {
77            // Merge new parameters with old overriding only those passed.
78            $this->_params = array_merge($this->_params, $params);
79        } else {
80            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_ERR, __FILE__, __LINE__);
81        }
82    }
83
84    /**
85     * Return the value of a parameter, if it exists.
86     *
87     * @access public
88     * @param string $param        Which parameter to return.
89     * @return mixed               Configured parameter value.
90     */
91    function getParam($param)
92    {
93        $app =& App::getInstance();
94   
95        if (isset($this->_params[$param])) {
96            return $this->_params[$param];
97        } else {
98            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
99            return null;
100        }
101    }
102
103    /**
104     * Setup the database table for this class.
105     *
106     * @access  public
107     * @author  Quinn Comendant <quinn@strangecode.com>
108     * @since   04 Jun 2006 16:41:42
109     */
110    function initDB($recreate_db=false)
111    {
112        $app =& App::getInstance();
113        $db =& DB::getInstance();
114
115        static $_db_tested = false;
116
117        if ($recreate_db || !$_db_tested && $this->getParam('create_table')) {
118
119            if ($recreate_db) {
120                $db->query("DROP TABLE IF EXISTS acl_tbl");
121                $db->query("DROP TABLE IF EXISTS aro_tbl");
122                $db->query("DROP TABLE IF EXISTS aco_tbl");
123                $db->query("DROP TABLE IF EXISTS axo_tbl");
124                $app->logMsg(sprintf('Dropping and recreating tables acl_tbl, aro_tbl, aco_tbl, axo_tbl.', null), LOG_DEBUG, __FILE__, __LINE__);
125            }
126           
127            // acl_tbl
128            $db->query("
129                CREATE TABLE IF NOT EXISTS acl_tbl (
130                    aro_id SMALLINT(11) UNSIGNED NOT NULL DEFAULT '0',
131                    aco_id SMALLINT(11) UNSIGNED NOT NULL DEFAULT '0',
132                    axo_id SMALLINT(11) UNSIGNED NOT NULL DEFAULT '0',
133                    access ENUM('allow', 'deny') DEFAULT NULL,
134                    added_datetime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
135                    UNIQUE KEY (aro_id, aco_id, axo_id),
136                    KEY (access)
137                ) ENGINE=MyISAM
138            ");
139            if (!$db->columnExists('acl_tbl', array(
140                'aro_id',
141                'aco_id',
142                'axo_id',
143                'access',
144                'added_datetime',
145            ), false, false)) {
146                $app->logMsg(sprintf('Database table acl_tbl has invalid columns. Please update this table manually.', null), LOG_ALERT, __FILE__, __LINE__);
147                trigger_error(sprintf('Database table acl_tbl has invalid columns. Please update this table manually.', null), E_USER_ERROR);
148            } else {
149                // Insert root node data if nonexistant, dely all by default.
150                $qid = $db->query("SELECT 1 FROM acl_tbl");
151                if (mysql_num_rows($qid) == 0) {
152                    $qid = $db->query("REPLACE INTO acl_tbl VALUES ('1', '1', '1', 'deny', NOW())");                   
153                }               
154            }
155
156            // aro_tbl, aco_tbl, axo_tbl
157            foreach (array('aro', 'aco', 'axo') as $a_o) {
158                $db->query("
159                    CREATE TABLE IF NOT EXISTS {$a_o}_tbl (
160                        {$a_o}_id SMALLINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
161                        name VARCHAR(32) NOT NULL DEFAULT '',
162                        lft MEDIUMINT(9) UNSIGNED NOT NULL DEFAULT '0',
163                        rgt MEDIUMINT(9) UNSIGNED NOT NULL DEFAULT '0',
164                        added_datetime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
165                        UNIQUE KEY name (name(15)),
166                        KEY transversal (lft, rgt)
167                    ) ENGINE=MyISAM;
168                ");
169
170                if (!$db->columnExists("{$a_o}_tbl", array(
171                    "{$a_o}_id",
172                    'name',
173                    'lft',
174                    'rgt',
175                    'added_datetime',
176                ), false, false)) {
177                    $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', "{$a_o}_tbl"), LOG_ALERT, __FILE__, __LINE__);
178                    trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', "{$a_o}_tbl"), E_USER_ERROR);
179                } else {
180                    // Insert root node data if nonexistant.
181                    $qid = $db->query("SELECT 1 FROM {$a_o}_tbl WHERE name = 'root'");
182                    if (mysql_num_rows($qid) == 0) {
183                        $qid = $db->query("REPLACE INTO {$a_o}_tbl (name, lft, rgt, added_datetime) VALUES ('root', 1, 2, NOW())");                   
184                    }                   
185                }
186
187            }
188        }
189        $_db_tested = true;
190        return true;
191    }
192
193    /*
194    *
195    *
196    * @access   public
197    * @param   
198    * @return   
199    * @author   Quinn Comendant <quinn@strangecode.com>
200    * @version  1.0
201    * @since    14 Jun 2006 22:39:29
202    */
203    function add($name, $parent=null, $type)
204    {
205        $app =& App::getInstance();
206        $db =& DB::getInstance();
207       
208        $this->initDB();
209       
210        switch ($type) {
211        case 'aro' :
212            $tbl = 'aro_tbl';
213            break;
214        case 'aco' :
215            $tbl = 'aco_tbl';
216            break;
217        case 'axo' :
218            $tbl = 'axo_tbl';
219            break;
220        default :
221            $app->logMsg(sprintf('Invalid access object type: %s', $type), LOG_ERR, __FILE__, __LINE__);
222            return false;
223            break;
224        }
225       
226        // If $parent is null, use root object.
227        if (is_null($parent)) {
228            $parent = 'root';
229        }
230       
231        // Ensure node and parent name aren't empty.
232        if ('' == trim($name) || '' == trim($parent)) {
233            $app->logMsg(sprintf('Cannot add node, parent (%s) or name (%s) missing.', $name, $parent), LOG_WARNING, __FILE__, __LINE__);
234            return false;
235        }
236       
237        // Ensure node is unique.
238        $qid = $db->query("SELECT 1 FROM $tbl WHERE name = '" . $db->escapeString($name) . "'");
239        if (mysql_num_rows($qid) > 0) {
240            $app->logMsg(sprintf('Cannot add %s node, already exists: %s', $type, $name), LOG_NOTICE, __FILE__, __LINE__);
241            return false;
242        }
243       
244        // Select the rgt of $parent.
245        $qid = $db->query("SELECT rgt FROM $tbl WHERE name = '" . $db->escapeString($parent) . "'");
246        if (!list($rgt) = mysql_fetch_row($qid)) {
247            $app->logMsg(sprintf('Cannot add %s node to nonexistant parent: %s', $type, $parent), LOG_WARNING, __FILE__, __LINE__);
248            return false;
249        }
250        // Update transversal numbers for all nodes to the rgt of $parent.
251        $db->query("UPDATE $tbl SET lft = lft + 2 WHERE lft >= $rgt");
252        $db->query("UPDATE $tbl SET rgt = rgt + 2 WHERE rgt >= $rgt");
253       
254        // Insert new node just below parent. Lft is parent's old rgt.
255        $db->query("
256            INSERT INTO $tbl (name, lft, rgt, added_datetime)
257            VALUES ('" . $db->escapeString($name) . "', $rgt, $rgt + 1, NOW())
258        ");
259
260        $app->logMsg(sprintf('Added %s node %s to parent %s.', $type, $name, $parent), LOG_DEBUG, __FILE__, __LINE__);
261        return mysql_insert_id($db->getDBH());
262    }
263
264    // Alias functions for the different object types.
265    function addARO($name, $parent=null)
266    {
267        return $this->add($name, $parent, 'aro');
268    }
269    function addACO($name, $parent=null)
270    {
271        return $this->add($name, $parent, 'aco');
272    }
273    function addAXO($name, $parent=null)
274    {
275        return $this->add($name, $parent, 'axo');
276    }
277
278    /*
279    *
280    *
281    * @access   public
282    * @param   
283    * @return   
284    * @author   Quinn Comendant <quinn@strangecode.com>
285    * @version  1.0
286    * @since    14 Jun 2006 22:39:29
287    */
288    function remove($name, $type)
289    {
290        $app =& App::getInstance();
291        $db =& DB::getInstance();
292       
293        $this->initDB();
294
295        switch ($type) {
296        case 'aro' :
297            $tbl = 'aro_tbl';
298            break;
299        case 'aco' :
300            $tbl = 'aco_tbl';
301            break;
302        case 'axo' :
303            $tbl = 'axo_tbl';
304            break;
305        default :
306            $app->logMsg(sprintf('Invalid access object type: %s', $type), LOG_ERR, __FILE__, __LINE__);
307            return false;
308            break;
309        }
310       
311        // Ensure node name isn't empty.
312        if ('' == trim($name)) {
313            $app->logMsg(sprintf('Cannot add node, name missing.', null), LOG_WARNING, __FILE__, __LINE__);
314            return false;
315        }
316       
317        // Select the lft of $name
318        $qid = $db->query("SELECT lft, rgt FROM $tbl WHERE name = '" . $db->escapeString($name) . "'");
319        if (!list($lft, $rgt) = mysql_fetch_row($qid)) {
320            $app->logMsg(sprintf('Cannot delete nonexistant %s name: %s', $type, $name), LOG_NOTICE, __FILE__, __LINE__);
321            return false;
322        }
323       
324        // Remove node and all children of node.
325        $db->query("DELETE FROM $tbl WHERE lft BETWEEN $lft AND $rgt");
326        $num_deleted_nodes = mysql_affected_rows($db->getDBH());
327
328        // Update transversal numbers for all nodes to the rgt of $parent, taking in to account the absence of it's children.
329        $db->query("UPDATE $tbl SET lft = lft - ($rgt - $lft + 1) WHERE lft > $lft");
330        $db->query("UPDATE $tbl SET rgt = rgt - ($rgt - $lft + 1) WHERE rgt > $rgt");
331
332        $app->logMsg(sprintf('Removed %s node %s along with %s children.', $type, $name, $num_deleted_nodes), LOG_DEBUG, __FILE__, __LINE__);
333        return true;
334    }
335   
336    // Alias functions for the different object types.
337    function removeUser($name, $parent=null)
338    {
339        return $this->remove($name, $parent, 'aro');
340    }
341    function removeAction($name, $parent=null)
342    {
343        return $this->remove($name, $parent, 'aco');
344    }
345    function removeObject($name, $parent=null)
346    {
347        return $this->remove($name, $parent, 'axo');
348    }
349   
350    /*
351    *
352    *
353    * @access   public
354    * @param   
355    * @return   
356    * @author   Quinn Comendant <quinn@strangecode.com>
357    * @version  1.0
358    * @since    15 Jun 2006 01:58:48
359    */
360    function grant($aro=null, $aco=null, $axo=null, $access='allow')
361    {
362        $app =& App::getInstance();
363        $db =& DB::getInstance();
364
365        $this->initDB();
366
367        // If any access objects are null, assume using root values.
368        // However if they're empty we don't want to escalate the grant command to root!
369        $aro = is_null($aro) ? 'root' : $aro;
370        $aco = is_null($aco) ? 'root' : $aco;
371        $axo = is_null($axo) ? 'root' : $axo;
372       
373        // Ensure values exist.
374        $qid = $db->query("SELECT aro_tbl.aro_id FROM aro_tbl WHERE aro_tbl.name = '" . $db->escapeString($aro) . "'");
375        if (!list($aro_id) = mysql_fetch_row($qid)) {
376            $app->logMsg(sprintf('Grant failed, aro_tbl.name %s does not exist.', $aro), LOG_WARNING, __FILE__, __LINE__);
377            return false;
378        }
379        $qid = $db->query("SELECT aco_tbl.aco_id FROM aco_tbl WHERE aco_tbl.name = '" . $db->escapeString($aco) . "'");
380        if (!list($aco_id) = mysql_fetch_row($qid)) {
381            $app->logMsg(sprintf('Grant failed, aco_tbl.name %s does not exist.', $aco), LOG_WARNING, __FILE__, __LINE__);
382            return false;
383        }
384        $qid = $db->query("SELECT axo_tbl.axo_id FROM axo_tbl WHERE axo_tbl.name = '" . $db->escapeString($axo) . "'");
385        if (!list($axo_id) = mysql_fetch_row($qid)) {
386            $app->logMsg(sprintf('Grant failed, axo_tbl.name %s does not exist.', $axo), LOG_WARNING, __FILE__, __LINE__);
387            return false;
388        }
389
390        // Access must be 'allow' or 'deny'.
391        $allow = 'allow' == $access ? 'allow' : 'deny';
392       
393        $db->query("REPLACE INTO acl_tbl VALUES ('$aro_id', '$aco_id', '$axo_id', '$allow', NOW())");
394       
395        return true;
396    }
397
398    /*
399    *
400    *
401    * @access   public
402    * @param   
403    * @return   
404    * @author   Quinn Comendant <quinn@strangecode.com>
405    * @version  1.0
406    * @since    15 Jun 2006 04:35:54
407    */
408    function revoke($aro=null, $aco=null, $axo=null)
409    {
410        return $this->grant($aro, $aco, $axo, 'deny');
411    }
412   
413    /*
414    *
415    *
416    * @access   public
417    * @param   
418    * @return   
419    * @author   Quinn Comendant <quinn@strangecode.com>
420    * @version  1.0
421    * @since    15 Jun 2006 03:58:23
422    */
423    function check($aro, $aco=null, $axo=null)
424    {
425        $app =& App::getInstance();
426        $db =& DB::getInstance();
427       
428        $this->initDB();
429
430        // If any access objects are null or empty, assume using root values.
431        $aro = is_null($aro) || '' == trim($aro) ? 'root' : $aro;
432        $aco = is_null($aco) || '' == trim($aco) ? 'root' : $aco;
433        $axo = is_null($axo) || '' == trim($axo) ? 'root' : $axo;
434       
435        $cache_hash = $aro . '|' . $aco . '|' . $axo;
436        if ($this->cache->exists($cache_hash) && true === $this->getParam('enable_cache')) {
437            // Access value is cached.
438            $access = $this->cache->get($cache_hash);
439        } else {
440            // Retreive access value from db.
441            $qid = $db->query("
442                SELECT acl_tbl.access
443                FROM acl_tbl
444                LEFT JOIN aro_tbl ON (acl_tbl.aro_id = aro_tbl.aro_id)
445                LEFT JOIN aco_tbl ON (acl_tbl.aco_id = aco_tbl.aco_id)
446                LEFT JOIN axo_tbl ON (acl_tbl.axo_id = axo_tbl.axo_id)
447                WHERE aro_tbl.lft <= (SELECT lft FROM aro_tbl WHERE name = '" . $db->escapeString($aro) . "')
448                AND aco_tbl.lft <= (SELECT lft FROM aco_tbl WHERE name = '" . $db->escapeString($aco) . "')
449                AND axo_tbl.lft <= (SELECT lft FROM axo_tbl WHERE name = '" . $db->escapeString($axo) . "')
450                ORDER BY aro_tbl.aro_id DESC, aco_tbl.aco_id DESC, axo_tbl.axo_id DESC
451                LIMIT 1
452            ");
453            if (!list($access) = mysql_fetch_row($qid)) {
454                $app->logMsg(sprintf('Access denyed: %s -> %s -> %s. No records found.', $aro, $aco, $axo), LOG_DEBUG, __FILE__, __LINE__);
455                return false;
456            }
457            $this->cache->set($cache_hash, $access);
458        }
459       
460        if ('allow' == $access) {
461            $app->logMsg(sprintf('Access granted: %s -> %s -> %s.', $aro, $aco, $axo), LOG_DEBUG, __FILE__, __LINE__);
462            return true;
463        } else {
464            $app->logMsg(sprintf('Access denyed: %s -> %s -> %s.', $aro, $aco, $axo), LOG_DEBUG, __FILE__, __LINE__);
465            return false;
466        }
467    }
468
469} // End class.
470
471
472?>
Note: See TracBrowser for help on using the repository browser.