permTable = $config['tblNamePrefix'].'perms'; $this->sessTable = $config['tblNamePrefix'].'sess'; } /* ======================================================= public methods */ /* ----------------------------------------------- session/authentication */ /** * Authenticate and create session * * @param login string * @param pass string * @return boolean/sessionId/err */ function login($login, $pass) { if(FALSE === $this->authenticate($login, $pass)) return FALSE; $sessid = $this->_createSessid(); if(PEAR::isError($sessid)) return $sessid; $userid = $this->getSubjId($login); $r = $this->dbc->query("INSERT INTO {$this->sessTable} (sessid, userid, login, ts) VALUES ('$sessid', '$userid', '$login', now())"); if(PEAR::isError($r)) return $r; $this->login = $login; $this->userid = $userid; $this->sessid = $sessid; return $sessid; } /** * Logout and destroy session * * @param sessid string * @return true/err */ function logout($sessid) { $ct = $this->checkAuthToken($sessid); if($ct === FALSE) return PEAR::raiseError('Alib::logout: not logged ($ct)', ALIBERR_NOTLOGGED, PEAR_ERROR_RETURN); elseif(PEAR::isError($ct)) return $ct; else{ $r = $this->dbc->query("DELETE FROM {$this->sessTable} WHERE sessid='$sessid'"); if(PEAR::isError($r)) return $r; $this->login = NULL; $this->userid = NULL; $this->sessid = NULL; return TRUE; } } /** * Return true if the token is valid * * @param sessid string * @return boolean/err */ function checkAuthToken($sessid) { $c = $this->dbc->getOne("SELECT count(*) as cnt FROM {$this->sessTable} WHERE sessid='$sessid'"); return ($c == 1 ? TRUE : (PEAR::isError($c) ? $c : FALSE )); } /** * Set valid token in alib object * * @param sessid string * @return boolean/err */ function setAuthToken($sessid) { $r = checkAuthToken($sessid); if(PEAR::isError($r)) return $r; if(!$r) return PEAR::raiseError("ALib::setAuthToken: invalid token ($sessid)"); $this->sessid = $sessid; return TRUE; } /* -------------------------------------------------------- authorization */ /** * Insert permission record * * @param sid int * @param action string * @param oid int * @param type char * @return int/err */ function addPerm($sid, $action, $oid, $type='A') { $permid = $this->dbc->nextId("{$this->permTable}_id_seq"); $r = $this->dbc->query($q = " INSERT INTO {$this->permTable} (permid, subj, action, obj, type) VALUES ($permid, $sid, '$action', $oid, '$type') "); if(PEAR::isError($r)) return($r); return $permid; } /** * Remove permission record * * @param permid int OPT * @param subj int OPT * @param obj int OPT * @return null/error */ function removePerm($permid=NULL, $subj=NULL, $obj=NULL) { $ca = array(); if($permid) $ca[] = "permid=$permid"; if($subj) $ca[] = "subj=$subj"; if($obj) $ca[] = "obj=$obj"; $cond = join(" AND ", $ca); if(!$cond) return TRUE; return $this->dbc->query("DELETE FROM {$this->permTable} WHERE $cond"); } /** * Check if specified subject have permission to specified action * on specified object * * Look for sequence of correnponding permissions and order it by * relevence, then test the most relevant for result. * High relevence have direct permission (directly for specified subject * and object. Relevance order is done by level distance in the object * tree, level distance in subjects (user/group system). * Similar way is used for permissions related to object classes. * But class-related permissions have lower priority then * object-tree-related. * Support for object classes can be disabled by USE_ALIB_CLASSES const. * * @param sid int, subject id (user or group id) * @param action string, from set defined in config * @param oid int, object id, optional (default: root node) * @return boolean/err */ function checkPerm($sid, $action, $oid=NULL) { if(!is_numeric($sid)) return FALSE; if(is_null($oid) or $oid=='') $oid = $this->getObjId($this->RootNode); if(PEAR::isError($oid)) return $oid; if(!is_numeric($oid)) return FALSE; // query construction // shortcuts: // p: permTable, // s: subjTable, m smembTable, // t: treeTable ts: structTable, // c: classTable, cm: cmembTable // main query elements: $q_flds = "m.level , p.subj, s.login, action, p.type, p.obj"; $q_from = "{$this->permTable} p "; // joins for solving users/groups: $q_join = "LEFT JOIN {$this->subjTable} s ON s.id=p.subj "; $q_join .= "LEFT JOIN {$this->smembTable} m ON m.gid=p.subj "; $q_cond = "p.action in('_all', '$action') AND (s.id=$sid OR m.uid=$sid) "; // coalesce -1 for higher priority of nongroup rows: // action DESC order for lower priority of '_all': $q_ordb = "ORDER BY coalesce(m.level,-1), action DESC, p.type DESC"; $q_flds0 = $q_flds; $q_from0 = $q_from; $q_join0 = $q_join; $q_cond0 = $q_cond; $q_ordb0 = $q_ordb; // joins for solving object tree: $q_flds .= ", t.name, ts.level as tlevel"; $q_join .= "LEFT JOIN {$this->treeTable} t ON t.id=p.obj "; $q_join .= "LEFT JOIN {$this->structTable} ts ON ts.parid=p.obj "; $q_cond .= " AND (t.id=$oid OR ts.objid=$oid)"; // action DESC order is hack for lower priority of '_all': $q_ordb = "ORDER BY coalesce(ts.level,0), m.level, action DESC, p.type DESC"; // query by tree: $query1 = "SELECT $q_flds FROM $q_from $q_join WHERE $q_cond $q_ordb"; $r1 = $this->dbc->getAll($query1); if(PEAR::isError($r1)) return($r1); // if there is row with type='A' on the top => permit $AllowedByTree = (is_array($r1) && count($r1)>0 && $r1[0]['type']=='A'); $DeniedByTree = (is_array($r1) && count($r1)>0 && $r1[0]['type']=='D'); if(!USE_ALIB_CLASSES) return $AllowedbyTree; // joins for solving object classes: $q_flds = $q_flds0.", c.cname "; $q_join = $q_join0."LEFT JOIN {$this->classTable} c ON c.id=p.obj "; $q_join .= "LEFT JOIN {$this->cmembTable} cm ON cm.cid=p.obj "; $q_cond = $q_cond0." AND (c.id=$oid OR cm.objid=$oid)"; $q_ordb = $q_ordb0; // query by class: $query2 = "SELECT $q_flds FROM $q_from $q_join WHERE $q_cond $q_ordb"; $r2 = $this->dbc->getAll($query2); if(PEAR::isError($r2)) return($r2); $AllowedByClass = (is_array($r2) && count($r2)>0 && $r2[0]['type']=='A'); // not used now: // $DeniedByClass = // (is_array($r2) && count($r2)>0 && $r2[0]['type']=='D'); $res = ($AllowedByTree || (!$DeniedByTree && $AllowedByClass)); # echo"
\nsid=$sid, action=$action, oid=$oid\n"; var_dump($r1); echo"\n---\n$query1\n---\n\n"; var_dump($r2); echo"\n---\n$query2\n---\n\n"; exit;
        return $res;
    }

    /* ---------------------------------------------------------- object tree */

    /**
     *   Remove all permissions on object and then remove object itself
     *
     *   @param id int
     *   @return void/error
     */
    function removeObj($id)
    {
        $r = $this->removePerm(NULL, NULL, $id);
        if(PEAR::isError($r)) return $r;
        return parent::removeObj($id);
    }

    /* --------------------------------------------------------- users/groups */

    /**
     *   Remove all permissions of subject and then remove subject itself
     *
     *   @param login string
     *   @return void/error
     */
    function removeSubj($login)
    {
        $uid = $this->getSubjId($login);    if(PEAR::isError($uid)) return $uid;
        if(is_null($uid)){
            return $this->dbc->raiseError("Alib::removeSubj: Subj not found ($login)",
                ALIBERR_NOTEXISTS,  PEAR_ERROR_RETURN);
        }
        $r = $this->removePerm(NULL, $uid); if(PEAR::isError($r)) return $r;
        return parent::removeSubj($login, $uid);
    }
    
    /* ------------------------------------------------------------- sessions */
    /**
     *   Get login from session id (token)
     *
     *   @param sessid string
     *   @return string/error
     */
    function getSessLogin($sessid)
    {
        return $this->dbc->getOne("
            SELECT login FROM {$this->sessTable} WHERE sessid='$sessid'");
    }

    /**
     *   Get user id from session id
     *
     *   @param sessid string
     *   @return int/error
     */
    function getSessUserId($sessid)
    {
        return $this->dbc->getOne("
            SELECT userid FROM {$this->sessTable} WHERE sessid='$sessid'");
    }

    /* --------------------------------------------------------- info methods */
    /**
     *   Get all permissions on object
     *
     *   @param id int
     *   @return array/null/err
     */
    function getObjPerms($id)
    {
        return $this->dbc->getAll("
            SELECT s.login, p.* FROM {$this->permTable} p, {$this->subjTable} s
            WHERE s.id=p.subj AND p.obj=$id");
    }

    /**
     *   Get all permissions of subject
     *
     *   @param sid int
     *   @return array
     */
    function getSubjPerms($sid)
    {
        $a1 = $this->dbc->getAll("
            SELECT t.name, t.type as otype , p.*
            FROM {$this->permTable} p, {$this->treeTable} t
            WHERE t.id=p.obj AND p.subj=$sid");
        if(PEAR::isError($a1)) return $a1;
        $a2 = $this->dbc->getAll("
            SELECT c.cname as name, 'C'as otype, p.*
            FROM {$this->permTable} p, {$this->classTable} c
            WHERE c.id=p.obj AND p.subj=$sid");
        if(PEAR::isError($a2)) return $a2;
        return array_merge($a1, $a2);
    }

    /* ------------------------ info methods related to application structure */
    /* (this part should be added/rewritten to allow defining/modifying/using
     * application structure)
     * (only very simple structure definition - in $config - supported now)
     */
    
    /**
     *   Get all actions
     *
     *   @return array
     */
    function getAllActions()
    {
        return $this->config['allActions'];
    }

    /**
     *   Get all allowed actions on specified object type
     *
     *   @param type string
     *   @return array
     */
    function getAllowedActions($type)
    {
        return $this->config['allowedActions'][$type];
    }

    /* ====================================================== private methods */
    
    /**
     *   Create new session id
     *
     *   @return string sessid
     */
    function _createSessid()
    {
        for($c=1; $c>0;){
            $sessid = md5(uniqid(rand()));
            $c = $this->dbc->getOne("SELECT count(*) FROM {$this->sessTable}
                WHERE sessid='$sessid'");
            if(PEAR::isError($c)) return $c;
        }
        return $sessid;
    }
    
    /* =============================================== test and debug methods */

    /**
     *   Dump all permissions for debug
     *
     *   @param indstr string    // indentation string
     *   @param ind string       // aktual indentation
     *   @return string
     */
    function dumpPerms($indstr='    ', $ind='')
    {
        $arr = $this->dbc->getAll("
            SELECT s.login, p.action, p.type
            FROM {$this->permTable} p, {$this->subjTable} s
            WHERE s.id=p.subj
            ORDER BY p.permid
        ");
        if(PEAR::isError($arr)) return $arr;
        $r = $ind.join(', ', array_map(create_function('$v',
                'return "{$v[\'login\']}/{$v[\'action\']}/{$v[\'type\']}";'
            ),
            $arr
        ))."\n";
        return $r;
    }
    
    /**
     *   deleteData
     *
     *   @return void
     */
    function deleteData()
    {
        $this->dbc->query("DELETE FROM {$this->permTable}");
        $this->dbc->query("DELETE FROM {$this->sessTable}");
        parent::deleteData();
    }
    /**
     *   Insert test permissions
     *
     *   @return array
     */
    function testData()
    {
        parent::testData();
        $t =& $this->tdata['tree'];
        $c =& $this->tdata['classes'];
        $s =& $this->tdata['subjects'];
        $this->dbc->setErrorHandling(PEAR_ERROR_PRINT);
        $perms = array(
            array($s['root'], '_all', $t['root'], 'A'),
            array($s['test1'], '_all', $t['pa'], 'A'),
            array($s['test1'], 'read', $t['s2b'], 'D'),
            array($s['test2'], 'addChilds', $t['pa'], 'D'),
            array($s['test2'], 'read', $t['i2'], 'A'),
            array($s['test2'], 'edit', $t['s1a'], 'A'),
            array($s['test1'], 'addChilds', $t['s2a'], 'D'),
            array($s['test1'], 'addChilds', $t['s2c'], 'D'),
            array($s['gr2'], 'addChilds', $t['i2'], 'A'),
            array($s['test3'], '_all', $t['t1'], 'D'),
        );
        if(USE_ALIB_CLASSES){
            $perms[] = array($s['test3'], 'read', $c['cl_sa'], 'D');
            $perms[] = array($s['test4'], 'editPerms', $c['cl2'], 'A');
        }
        foreach($perms as $p){
            $o[] = $r = $this->addPerm($p[0], $p[1], $p[2], $p[3]);
            if(PEAR::isError($r)) return $r;
        }
        $this->tdata['perms'] = $o;
    }
    
    /**
     *   Make basic test
     *
     *   @return boolean/error
     */
    function test()
    {
        if(PEAR::isError($p = parent::test())) return $p;
        $this->deleteData();
        $r = $this->testData();
        if(PEAR::isError($r)) return $r;
        $this->test_correct = "root/_all/A, test1/_all/A, test1/read/D,".
            " test2/addChilds/D, test2/read/A, test2/edit/A,".
            " test1/addChilds/D, test1/addChilds/D, gr2/addChilds/A,".
            " test3/_all/D";
        if(USE_ALIB_CLASSES){
            $this->test_correct .= ", test3/read/D, test4/editPerms/A";
        }
        $this->test_correct .= "\nno, yes\n";
        $r = $this->dumpPerms();
        if(PEAR::isError($r)) return $r;
        $this->test_dump = $r.
            ($this->checkPerm(
                $this->tdata['subjects']['test1'], 'read',
                $this->tdata['tree']['t1']
            )? 'yes':'no').", ".
            ($this->checkPerm(
                $this->tdata['subjects']['test1'], 'addChilds',
                $this->tdata['tree']['i2']
            )? 'yes':'no')."\n"
        ;
        $this->removePerm($this->tdata['perms'][1]);
        $this->removePerm($this->tdata['perms'][3]);
        $this->test_correct .= "root/_all/A, test1/read/D,".
            " test2/read/A, test2/edit/A,".
            " test1/addChilds/D, test1/addChilds/D, gr2/addChilds/A,".
            " test3/_all/D";
        if(USE_ALIB_CLASSES){
            $this->test_correct .= ", test3/read/D, test4/editPerms/A";
        }
        $this->test_correct .= "\n";
        $this->test_dump .= $this->dumpPerms();
        $this->deleteData();
        if($this->test_dump==$this->test_correct)
        { $this->test_log.="alib: OK\n"; return TRUE;
        }else return PEAR::raiseError('Alib::test', 1, PEAR_ERROR_DIE, '%s'.
            "
\ncorrect:\n{$this->test_correct}\n".
            "dump:\n{$this->test_dump}\n
\n"); } /** * Create tables + initialize * * @return void */ function install() { parent::install(); $this->dbc->query("CREATE TABLE {$this->permTable} ( permid int not null PRIMARY KEY, subj int REFERENCES {$this->subjTable} ON DELETE CASCADE, action varchar(20), obj int, type char(1) )"); $this->dbc->query("CREATE UNIQUE INDEX {$this->permTable}_permid_idx ON {$this->permTable} (permid)"); $this->dbc->query("CREATE INDEX {$this->permTable}_subj_obj_idx ON {$this->permTable} (subj, obj)"); $this->dbc->query("CREATE UNIQUE INDEX {$this->permTable}_all_idx ON {$this->permTable} (subj, action, obj)"); $this->dbc->createSequence("{$this->permTable}_id_seq"); $this->dbc->query("CREATE TABLE {$this->sessTable} ( sessid char(32) not null PRIMARY KEY, userid int REFERENCES {$this->subjTable} ON DELETE CASCADE, login varchar(255), ts timestamp )"); $this->dbc->query("CREATE UNIQUE INDEX {$this->sessTable}_sessid_idx ON {$this->sessTable} (sessid)"); $this->dbc->query("CREATE INDEX {$this->sessTable}_userid_idx ON {$this->sessTable} (userid)"); $this->dbc->query("CREATE INDEX {$this->sessTable}_login_idx ON {$this->sessTable} (login)"); } /** * Drop tables etc. * * @return void */ function uninstall() { $this->dbc->query("DROP TABLE {$this->permTable}"); $this->dbc->dropSequence("{$this->permTable}_id_seq"); $this->dbc->query("DROP TABLE {$this->sessTable}"); parent::uninstall(); } } ?>