Home > Development > Simple RBAC using MongoDB and CakePHP

Simple RBAC using MongoDB and CakePHP

I was tasked with implementing RBAC for a CakePHP application. After setting up CakePHP’s ACL I decided that MongoDB might be able to allow for an easier solution than MySQL. This is the first iteration and can probably stand for some improvement.

Requirements

Step 1
Create a MongoDB database
Create two collections, groups, users

Step 2
Create Models and Controllers for the collections.

Group Model

class Group extends AppModel
{
 
    public $displayField = 'group';
    public $hasMany = array('User');
    var $mongoSchema = array(
        'group' => array('type' => 'string'),
        'controllers' => array('type' => 'array'),
        'defaultController' => array('type' => 'string'),
        'defaultAction' => array('type' => 'string'),
        'created' => array('type' => 'datetime'),
        'modified' => array('type' => 'datetime')
    );
    public $validate = array(
        'group' => array(
            'isUnique' => array(
                'rule' => 'isUnique',
                'required' => true,
                'message' => 'Duplicate Group',
            ),
            'notEmpty' => array(
                'rule' => 'notEmpty',
                'required' => true,
                'message' => 'Group name is required',
            )
        )
    );
 
    /**
     * 
     */
    public function afterDelete()
    {
        parent::afterDelete();
        $this->deleteGroupFromUsers();
    }
 
    /**
     * This method searches all the User's group array for the current Group's ID and then 
     * deletes that record. This is typically done after deleting a group to prevent
     * orphan IDs in the User's account.
     */
    public function deleteGroupFromUsers()
    {
        $users = $this->User->find('all');
        foreach ($users as &$user)
        {
            foreach ($user['User']['groups'] as $i => &$group_id)
            {
                if ($group_id == $this->id)
                {
                    unset($user['User']['groups'][$i]);
                    $this->User->save($user);
                }
            }
        }
    }
 
}

User Model

class User extends AppModel
{
 
    public $belongsTo = array('Group');
    public $displayField = 'username';
    public $mongoSchema = array(
        'username' => array('type' => 'string'),
        'password' => array('type' => 'string'),
        'groups' => array('type' => 'array'),
        'created' => array('type' => 'datetime'),
        'modified' => array('type' => 'datetime')
    );
    /*
    public $validate = array(
        'username' => array(
            'isUnique' => array(
                'rule' => 'isUnique',
                'required' => true,
                'message' => 'Duplicate username'
            )
        ),
        'password' => array(
            'isUnique' => array(
                'rule' => 'isUnique',
                'required' => true,
                'message' => 'Password is required'
            )
        ),
        'groups' => array(
            'notEmpty' => array(
                'rule' => 'notEmpty',
                'required' => true,
                'message' => 'You must choose at least one group'
            )
        )
    );*/
 
}

Groups Controller

class GroupsController extends AppController
{
 
    /**
     *
     * @var type 
     */
    public $name = 'Groups';
 
    /**
     *
     * @var type 
     */
    public $paginate = array(
        'limit' => 15,
        'order' => array(
            'Group.group' => 'asc'
        )
    );
 
    public function __construct()
    {
        parent::__construct();
    }
 
    function beforeFilter()
    {
        parent::beforeFilter();
    }
 
    public function index()
    {
        $results = $this->paginate('Group');
        $this->set(compact('results'));
    }
 
    /**
     * add method
     *
     * @return void
     * @access public
     */
    public function add()
    {
        if (!empty($this->data))
        {
            $this->_cleanupPerms();
            $this->Group->create();
            if ($this->Group->validates())
            {
                if ($this->Group->save($this->data))
                {
                    $this->flash(__('Group Created.', true), array('action' => 'index'));
                }
            }
        }
 
        $this->set('controllers', $this->get_controllers_actions());
    }
 
    public function edit($id = null)
    {
        if (!$id && empty($this->data))
        {
            $this->flash(__('Invalid Group', true), array('action' => 'index'));
        }
 
        if (!empty($this->data))
        {
            $this->_cleanupPerms();
            // if ($this->Group->validates())
            //{
            if ($this->Group->save($this->data))
            {
                $this->flash(__('The Group has been saved.', true), array('action' => 'index'));
            }
            else
            {
                // @TODO need to handle when group is not saved
            }
            //}
        }
 
        if (empty($this->data))
        {
            $this->data = $this->Group->read(null, $id);
        }
 
        $this->set('controllers', $this->get_controllers_actions());
    }
 
    public function delete($id = null)
    {
        // @TODO Dont forget to come up with a cleaning function to purge deleted groups from the Users collection
        if (!$id)
        {
            $this->flash(__('Invalid Group', true), array('action' => 'index'));
        }
        if ($this->Group->delete($id))
        {
            $this->flash(__('Group deleted', true), array('action' => 'index'));
        }
        else
        {
            $this->flash(__('Group deleted Fail', true), array('action' => 'index'));
        }
    }
 
    /**
     * Uset any permissions that are set to 0. These come from checkboxes so 
     * possible values are 1 - selected or 0 - not selected
     * 
     * Unset any controllers that have no perms selected
     */
    protected function _cleanupPerms()
    {
        foreach ($this->data['Group']['controllers'] as $controller => &$perms)
        {
            foreach ($perms as $action => $perm)
            {
                if ($perm == 0)
                {
                    unset($perms[$action]);
                }
            }
            // If there are no perms in the controller then unset that bitch.
            if (empty($this->data['Group']['controllers'][$controller]))
            {
                unset($this->data['Group']['controllers'][$controller]);
            }
        }
    }
 
}

Users Controller

class UsersController extends AppController
{
 
    /**
     *
     * @var type 
     */
    public $name = 'Users';
 
    /**
     *
     * @var type 
     */
    public $paginate = array(
        'limit' => 15,
        'order' => array(
            'User.username' => 'asc'
        )
    );
 
    public function __construct()
    {
        parent::__construct();
    }
 
    function beforeFilter()
    {
        parent::beforeFilter();
    }
 
    public function index()
    {
        $results = $this->paginate('User');
        $this->set(compact('results'));
    }
 
    function login()
    {
        if ($this->Auth->login($this->data))
        {
            /*
             * You must do this to load the ACL data into session or the user
             * will not have permission to do anything.
             */
            $this->SimpleAcl->loadAcl();
            $this->redirect($this->Auth->loginRedirect, null, false);
        }
    }
 
    function logout()
    {
        $this->Session->destroy();
        $this->redirect($this->Auth->logout());
    }
 
    /**
     * add method
     *
     * @return void
     * @access public
     */
    public function add()
    {
        if (!empty($this->data))
        {
            if ($this->data['User']['password'] == $this->Auth->password($this->data['User']['password_confirm']))
            {
                $this->User->create();
 
                //if ($this->User->validates())
                //{
                if ($this->User->save($this->data))
                {
                    $this->flash(__('Account Created.', true), array('action' => 'index'));
                }
                // }
            }
        }
        $this->set('groups', $this->User->Group->find('list'));
    }
 
    public function edit($id = null)
    {
        if (!$id && empty($this->data))
        {
            $this->flash(__('Invalid User', true), array('action' => 'index'));
        }
 
        if (!empty($this->data))
        {
            //if ($this->User->validates())
            //{
            if ($this->User->save($this->data))
            {
                $this->flash(__('The User has been saved.', true), array('action' => 'index'));
            }
            else
            {
                // @TODO need to handle when group is not saved
            }
            // }
        }
 
        if (empty($this->data))
        {
            $this->data = $this->User->read(null, $id);
        }
 
        $this->set('groups', $this->User->Group->find('list'));
    }
 
    public function reset_password()
    {
 
    }
}

And finally the SimpleRBAC Component

/**
 * MongoDB RBAC/ACL
 * 
 * Expects that Cake's Auth component will handle logging the user in and creating
 * the User session.
 * 
 * To use this component you need two models Users, Groups that are tied to a 
 * MongoDB.
 * 
  class Group extends AppModel
  {
 
  public $displayField = 'group';
  public $hasMany = array('User');
  var $mongoSchema = array(
  'group' => array('type' => 'string'),
  'controllers' => array('type' => 'array'),
  'defaultController' => array('type' => 'string'),
  'defaultAction' => array('type' => 'string'),
  'created' => array('type' => 'datetime'),
  'modified' => array('type' => 'datetime')
  );
 
  }
 * 
  class User extends AppModel
  {
 
  public $belongsTo = array('Group');
  public $displayField = 'username';
  public $mongoSchema = array(
  'username' => array('type' => 'string'),
  'password' => array('type' => 'string'),
  'groups' => array('type' => 'array'),
  'created' => array('type' => 'datetime'),
  'modified' => array('type' => 'datetime')
  );
  }
 * 
 * You must have a UsersController and its login method should contain at least
 * this
 * 
  function login()
  {
  if ($this->Auth->login($this->data))
  {
 
  //You must do this to load the ACL data into session or the user
  //will not have permission to do anything.
 
  $this->SimpleAcl->loadAcl();
  $this->redirect($this->Auth->loginRedirect, null, false);
  }
  }
 * 
 *
 * You must have a GroupsController and it should contain the following method. This
 * method must be called before writing group information to the DB
 * 
 
  protected function cleanupPerms()
  {
  foreach ($this->data['Group']['controllers'] as $controller => &$perms)
  {
  foreach ($perms as $action => $perm)
  {
  if ($perm == 0)
  {
  unset($perms[$action]);
  }
  }
  // If there are no perms in the controller then unset that bitch.
  if(empty($this->data['Group']['controllers'][$controller]))
  {
  unset($this->data['Group']['controllers'][$controller]);
  }
 
  }
  }
 * 
 * 
 * Your AppController::beforeFilter should contain at least this
 * 
  if(!$this->SimpleAcl->check())
  {
  if(!$this->Session->check('Auth.User.defaultPath'))
  {
  $this->redirect($this->Auth->loginRedirect);
  }
  else
  {
  $this->redirect($this->Session->read('Auth.User.defaultPath'));
  }
  }
 * 
 */
class SimpleAclComponent extends Object
{
 
    public $components = array('Session', 'Auth');
 
    /**
     *
     * @var array
     */
    public $allowedActions = array('login', 'logout');
 
    /**
     *
     * @var string 
     */
    public $userModel = 'User';
 
    /**
     *
     * @var string
     */
    public $groupModel = 'Group';
 
    /**
     *
     * @var controller instance
     */
    public $controller;
 
    /**
     * 
     * 
     *
     * @param object $controller A reference to the instantiating controller object
     * @return void
     * @access public
     */
    function initialize(&$controller, $settings = array())
    {
        $this->controller = &$controller;
 
        if (is_array($settings))
        {
            foreach ($settings as $setting => $values)
            {
                // Only add properties that exist for the class
                // This prevents polution of the class
                if (property_exists(get_class($this), $setting))
                {
                    $this->$setting = $values;
                }
            }
        }
    }
 
    /**
     * Main execution method.  Handles redirecting of invalid users, and processing
     * of login form data.
     *
     * @param object $controller A reference to the instantiating controller object
     * @return boolean
     * @access public
     */
    function startup(&$controller)
    {
 
    }
 
    /**
     * Find the requested controller/action in Auth.User.AclPerms. If its not
     * found then redirect the user back to the referring URL
     * 
     * AclPerms is a list of Group
     * 
     * @return boolean
     */
    public function check()
    {
        //var_dump($this->Session->read('Auth.User.AclPerms'));
 
        /*
         * Check to see if we need to do any checking based upon the value
         * of Auth::AllowActions vs. the current request
         * 
         * If the current controller and action are in the allowed actions array
         * then return true
         * 
         */
        if (in_array(strtolower($this->controller->name), array_keys($this->allowedActions)))
        {
            if (in_array($this->controller->action, $this->allowedActions[strtolower($this->controller->name)]))
            {
                return true;
            }
        }
        /*
         * Find the requested controller/action in Auth.User.AclPerms
         */
        $currentController = $this->controller->name;
        $currentAction = $this->controller->action;
        if ($this->Session->check('Auth.User.AclPerms'))
        {
            foreach ($this->Session->read('Auth.User.AclPerms') as $group)
            {
                foreach ($group['Group']['controllers'] as $controller => $perms)
                {
                    if ($currentController == $controller)
                    {
                        foreach ($perms as $k => $v)
                        {
                            if ($k == $currentAction && $v == '1')
                            {
                                return true;
                            }
                        }
                    }
                }
            }
        }
        /*
         * Set a semaphore in session so that we can trigger a dialog box
         * after the redirect happens
         */
        $this->Session->write('rbacDenied', true);
        /**
         * Keep the user on the same page by redirecting them back to the referer.
         */
        $this->controller->redirect($this->controller->referer());
    }
 
    /**
     * Loads the Group permissions into the User object in session
     */
    public function loadAcl()
    {
        $aclPerms = array();
        /*
         * Look for the groups array in the User session.
         * If it does not exists thrown an error
         */
        if (!$this->Session->check('Auth.User.groups'))
        {
            $this->cakeError('simpleAclError', array('msg' => 'No groups found in user record'));
        }
 
        /*
         * Look for the simpleAcl element in the User object in session.
         * If its not there then get it and insert it into the record
         * If you cant get it throw an error.
         */
        if (!$this->Session->check('Auth.User.AclPerms'))
        {
            foreach ($this->Session->read('Auth.User.groups') as $group_id)
            {
                $params = array(
                    'conditions' => array(
                        '_id' => $group_id
                    ),
                    'fields' => array(
                        'controllers' => 1,
                        'group' => 1,
                        'defaultController' => 1,
                        'defaultAction' => 1
                    )
                );
 
                $aclPerms[] = $this->controller->User->Group->find('first', $params);
            }
 
            if (empty($aclPerms))
            {
                $this->cakeError('simpleAclError', array('msg' => 'No groups found'));
            }
            /*
             * If the User is in more than one group the default and controller
             * is the root of the application. Otherwise redirect the user to
             * the group's default controller/action
             * 
             */
            if (count($aclPerms) == 1)
            {
                $c = $aclPerms[0]['Group']['defaultController'];
                $a = $aclPerms[0]['Group']['defaultAction'];
                $this->Session->write('Auth.User.defaultPath', array('controller' => $c, 'action' => $a));
                /*
                 * A one time redirect for after login
                 */
                $this->Auth->loginRedirect = array('controller' => $c, 'action' => $a);
            }
        }
 
        $this->Session->write('Auth.User.AclPerms', $aclPerms);
    }
 
    /**
     * Component shutdown.  If user is logged in, wipe out redirect.
     *
     * @param object $controller Instantiating controller
     * @access public
     */
    function shutdown(&$controller)
    {
 
    }
 
}

Add the following to your AppController

    public $components = array(
        'SimpleAcl' => array(
            'allowedActions' => array(
                'users' => array(
                    'login', 'logout'
                )
            )
        ),
        'RequestHandler',
        'Session',
        'Auth' => array(
            'loginRedirect' => array('controller' => 'main', 'action' => 'index'),
            'autoRedirect' => false
        )
    );

Add the following to the AppController::beforeFilter()

public function beforeFilter()
    {
        parent::beforeFilter();
        Security::setHash('md5');
        //$this->Auth->allow('*');
 
        /*
         * Check the user's request against the group's loaded permissions. If
         * this fails the user will be redirected to the referring URL
         */
        $this->SimpleAcl->check();
        /*
         * Check to see if the semaphore has been set and if its true. If so
         * show the user a dialog box.
         */
        if ($this->Session->check('rbacDenied') && $this->Session->read('rbacDenied') == true)
        {
            $this->Session->delete('rbacDenied');
            $this->dialogMessage(
                    array(
                        'title' => 'No Access',
                        'status' => 'warning',
                        'msg' => 'You dont have access to that.'
                    )
            );
        }
 
    }

Comments are closed.

TOP