Getting permissions from table?

Hii there,

I’m working on my own private CMS, and now, I want to get started with the dashboard.
For this, I have created the following schema:
schema

Now what I want to do is check all the permissions a user has through the role (currently, no per-user permissions are being used).
For example, take the following few roles:

  • Member has no permissions.
  • Editor has the permissions access_cms, show_posts, add_posts and edit_posts.
  • Manager has the access_cms, show_users, add_users, edit_users, remove_users and show_posts permissions.
  • Admin has all permissions.

Normally, I would just check for the role a user has and thus check whether the user has the permissions needed, but there is a catch!
As you can see, I used a 1:n relationship, meaning that 1 user can have n (multiple) roles.
So now I have to iterate through all the roles a user has and their permissions to see whether the user has the right permissions.
Taking the above roles:

  • User 1 has the Admin role.
  • User 2 has both the Editor and Manager roles.
  • User 3 has both the Member and Editor roles.

The rules I want to check if the user has the permission or not is very simple:

If you have it, you keep it.

Meaning that once you have a specific permission, you can’t lose it during the check.
So User 2 won’t lose it’s edit_users because he also has the Member role (which does not have this permission).

Now my question is, how would I do this in a CakePHP3 controller?
I hope somebody can help me with doing this in a mostly clean way.

IMHO you over complicated things, all of this you could just do with creating access_rights field in users and use bit operations,

// UsersTable
class UsersTable extends Table {
   public const ALLOW_EDIT_USER = 1;
   public const ALLOW_ADD_USER = 2;
   public const ALLOW_REMOVE_USER = 4;
...
}

// in Entity\User.php
public function addRight($right)
{
   $this->access_rights |= $right;
}

public function removeRight($right)
{
   $this->access_rights &= ~$right;
}

protected function _getCanAddUser() 
{
   return ($this->access_rights & UsersTable::ALLOW_ADD_USER) === UsersTable::ALLOW_ADD_USER;
}
//etc..

if you want to still use your way, just create virtual property (like _getCanAddUser) that iterates over all of your user rights and check for first true value

The thing is, this can’t (and probably shouldn’t) be hardcoded as I want to be able to edit user’s their roles (and roles themselves) from within the dashboard.
User’s and their roles are a bit too dynamic to hardcode it into the site itself.

hardcoded? not sure if i understand how. The way i showed is to compress all of ‘Roles & Permissions’ into 1 field in users table, you could even use it side by side with what you are using and use it as sort of ‘cached’ field so you dont have to get associated role tables on every user action

Ah ok, it looked like it was hardcoding stuff to me :slight_smile:

Could you explain to me what your code does?
What I can understand is that you have 3 constants (1 for each permission).
When you add a permission, you do a bitwise or?
When you remove a right, you do a bitwise and followed by a bitwise not?
Then when you check if the permission, you check whether the permission is the same as the requirement for the ALLOW_ADD_USER?
Am I correct?

And why this way? wouldn’t it break everything when a user has multiple roles?
And wouldn’t it make the code HUGE if I want to add more permissions down the road?

What I can understand is that you have 3 constants

the constants are my silly naming of your fields in database, so access_cms for me would be RIGHT_ACCESS_CMS, add_posts = RIGHT_ADD_POST

When you add a permission, you do a bitwise or?
When you remove a right, you do a bitwise and followed by a bitwise not?

its old technique more or less explained https://en.wikipedia.org/wiki/Bit_field , there is no implementation of ENUM in cake because its non standard sql type but if you get Enums its the same

And why this way? wouldn’t it break everything when a user has multiple roles?

since you want to check if its has that permission in any role its the same as just setting it to 1,
in math terms: 0 or 1 or 1 = 1

And wouldn’t it make the code HUGE if I want to add more permissions down the road?

you can add generic check to right

public function hasRight($right) 
{
   return $this->access_rights & right;
}

and as i said if you want to keep your way

public function hasRight(string $right) 
{
   return collection($this->users_roles)->firstMatch([
       '{*}.roles.{*}.permissions.' . $right => 1   
   ]) !== null;
}

https://book.cakephp.org/3.0/en/core-libraries/collections.html#Cake\Collection\Collection::firstMatch

note im not 100% sure about the path but it should give you the idea, and you need to be sure that user have contained [‘UsersRoles’ => … ] before checking it

Thanks for your explanation :slight_smile: (knowing why it works is worth more as getting it to work by itself)
I’ll go fiddle with it in the coming few days and update this thread if needed :slight_smile:

so, I build the following piece of code:

<?php
namespace App\Controller;

class AdminController extends AppController {
  public function initialize(){
    parent::initialize();
    $this->layout = 'admin';

    $this->loadModel('UsersRoles');

    // Get all roles for the current user
    $this->user_roles = $this->UsersRoles->find('all')->where(['user_id' => $this->Auth->user('id')])->contain('Roles');
  }

  public function index() {
    var_dump($this->hasRight("access_cms"));
  }

  public function hasRight(string $right){
    return collection($this->user_roles)->firstMatch([
      '{*}.roles.{*}.permissions.' . $right => 1   
    ]) !== null;
  }
}

However, the var_dump() on line 16 always returns false, even though the user does have this on true in one of its roles.
After looking into it, it probably is because I didn’t get the content from the permissions table anywhere…
Any clue on this?

hasRight was suppose to be in Entity/User.php, and you should be getting

$user = $this->Users->find()->contain(['UsersRoles' => ... ])->first();

// and then check with
if ($user->hasRight('access_cms'))

In the ['UsersRoles' => ... ] you have those ..., but what should I replace them with?
If I just remove the => ... (so it becomes ->contain(['UsersRoles'])), I get the error

Cannot match provided foreignKey for “UsersRoles”, got “(role_id)” but expected foreign key for “(user_id, role_id)”

Thanks for being so patient with me btw :slight_smile:

i basicly assume its ‘UsersRoles’ because i dont see it in your screenshot (users…), as for ... its the rest of associations => ['Roles' => ['Permissions']] im just lazy to type and also im not sure about your associations

Yes, it’s UsersRoles (the table is called users_roles).
So I now have ...->contain(['UsersRoles' => ['Roles' => ['Permissions']]])->first();, but I still get the error

Cannot match provided foreignKey for “UsersRoles”, got “(role_id)” but expected foreign key for “(user_id, role_id)”

There is nothing else to the screenshot that I provided in the OP (the association that goes to the left has to do with blog articles).

This is my current AdminController.php:

<?php
namespace App\Controller;

class AdminController extends AppController {
  public function initialize(){
    parent::initialize();
    $this->layout = 'admin';

    $this->loadModel('UsersRoles');
    $this->loadModel('Users');
  }

  public function index() {
    $user_roles = $this->Users->find()->contain(['UsersRoles' => ['Roles' => ['Permissions']]])->first();;
    //var_dump($user_roles->hasRight("access_cms"));
  }
}

did you bake your ‘UsersRoles’ model?

yea, I did:

UsersTable.php:

<?php
namespace App\Model\Table;

use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
use Cake\Event\Event;
use Cake\Datasource\EntityInterface;


/**
 * Users Model
 *
 * @property \App\Model\Table\ArticlesTable|\Cake\ORM\Association\HasMany $Articles
 * @property |\Cake\ORM\Association\BelongsToMany $Roles
 *
 * @method \App\Model\Entity\User get($primaryKey, $options = [])
 * @method \App\Model\Entity\User newEntity($data = null, array $options = [])
 * @method \App\Model\Entity\User[] newEntities(array $data, array $options = [])
 * @method \App\Model\Entity\User|bool save(\Cake\Datasource\EntityInterface $entity, $options = [])
 * @method \App\Model\Entity\User patchEntity(\Cake\Datasource\EntityInterface $entity, array $data, array $options = [])
 * @method \App\Model\Entity\User[] patchEntities($entities, array $data, array $options = [])
 * @method \App\Model\Entity\User findOrCreate($search, callable $callback = null, $options = [])
 *
 * @mixin \Cake\ORM\Behavior\TimestampBehavior
 */
class UsersTable extends Table
{

    /**
     * Initialize method
     *
     * @param array $config The configuration for the Table.
     * @return void
     */
    public function initialize(array $config)
    {
        parent::initialize($config);

        $this->setTable('users');
        $this->setDisplayField('id');
        $this->setPrimaryKey('id');

        $this->addBehavior('Timestamp');

        $this->hasMany('Articles', [
            'foreignKey' => 'user_id'
        ]);
        $this->belongsToMany('UsersRoles', [
            'foreignKey' => 'user_id',
            'targetForeignKey' => 'role_id',
            'joinTable' => 'users_roles'
        ]);
        $this->belongsTo('UsersDetails', [
          'foreignKey' => 'id',
          'joinType' => 'INNER'
      ]);
    }

    /**
     * Default validation rules.
     *
     * @param \Cake\Validation\Validator $validator Validator instance.
     * @return \Cake\Validation\Validator
     */
    public function validationDefault(Validator $validator)
    {
        $validator
            ->integer('id')
            ->allowEmpty('id', 'create')
            ->add('id', 'unique', ['rule' => 'validateUnique', 'provider' => 'table']);

        $validator
            ->scalar('username')
            ->maxLength('username', 64)
            ->requirePresence('username', 'create')
            ->notEmpty('username')
            ->add('username', 'unique', ['rule' => 'validateUnique', 'provider' => 'table']);

        $validator
            ->email('email')
            ->requirePresence('email', 'create')
            ->notEmpty('email')
            ->add('email', 'unique', ['rule' => 'validateUnique', 'provider' => 'table']);

        $validator
            ->scalar('password')
            ->maxLength('password', 255)
            ->requirePresence('password', 'create')
            ->notEmpty('password');

        return $validator;
    }

    /**
     * Returns a rules checker object that will be used for validating
     * application integrity.
     *
     * @param \Cake\ORM\RulesChecker $rules The rules object to be modified.
     * @return \Cake\ORM\RulesChecker
     */
    public function buildRules(RulesChecker $rules)
    {
        $rules->add($rules->isUnique(['username']));
        $rules->add($rules->isUnique(['email']));
        $rules->add($rules->isUnique(['id']));

        return $rules;
    }

    /**
     * Add the role for a user after it has been saved
     * https://discourse.cakephp.org/t/insert-with-related-rable/4384
     */
    public function afterSave(Event $event, EntityInterface $entity, \ArrayObject $options) {
      if($entity->isNew()) {
        $usersRole = $this->UsersRoles->newEntity();
        $usersRole->user_id = $entity->id;
        $usersRole->role_id = 1; // TODO: allow changing this value through a setting while creating a user using the admin dashboard(using $options?).
        $this->UsersRoles->save($usersRole);
      }
    }
}

UsersRole.php:

<?php
namespace App\Model\Entity;

use Cake\ORM\Entity;

/**
 * UsersRole Entity
 *
 * @property int $user_id
 * @property int $role_id
 *
 * @property \App\Model\Entity\User $user
 * @property \App\Model\Entity\Role $role
 */
class UsersRole extends Entity
{

    /**
     * Fields that can be mass assigned using newEntity() or patchEntity().
     *
     * Note that when '*' is set to true, this allows all unspecified fields to
     * be mass assigned. For security purposes, it is advised to set '*' to false
     * (or remove it), and explicitly make individual fields accessible as needed.
     *
     * @var array
     */
    protected $_accessible = [
        'user' => true,
        'role' => true
    ];
}

its UsersTable nor UsersRoleTable

Whoops, wrong file :slight_smile:
but yea UsersRolesTable is there as well:

<?php
namespace App\Model\Table;

use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;

/**
 * UsersRoles Model
 *
 * @property \App\Model\Table\UsersTable|\Cake\ORM\Association\BelongsTo $Users
 * @property \App\Model\Table\RolesTable|\Cake\ORM\Association\BelongsTo $Roles
 *
 * @method \App\Model\Entity\UsersRole get($primaryKey, $options = [])
 * @method \App\Model\Entity\UsersRole newEntity($data = null, array $options = [])
 * @method \App\Model\Entity\UsersRole[] newEntities(array $data, array $options = [])
 * @method \App\Model\Entity\UsersRole|bool save(\Cake\Datasource\EntityInterface $entity, $options = [])
 * @method \App\Model\Entity\UsersRole patchEntity(\Cake\Datasource\EntityInterface $entity, array $data, array $options = [])
 * @method \App\Model\Entity\UsersRole[] patchEntities($entities, array $data, array $options = [])
 * @method \App\Model\Entity\UsersRole findOrCreate($search, callable $callback = null, $options = [])
 */
class UsersRolesTable extends Table
{

    /**
     * Initialize method
     *
     * @param array $config The configuration for the Table.
     * @return void
     */
    public function initialize(array $config)
    {
        parent::initialize($config);

        $this->setTable('users_roles');
        $this->setDisplayField('user_id');
        $this->setPrimaryKey(['user_id', 'role_id']);

        $this->belongsTo('Users', [
            'foreignKey' => 'user_id',
            'joinType' => 'INNER'
        ]);
        $this->belongsTo('Roles', [
            'foreignKey' => 'role_id',
            'joinType' => 'INNER'
        ]);
    }

    /**
     * Returns a rules checker object that will be used for validating
     * application integrity.
     *
     * @param \Cake\ORM\RulesChecker $rules The rules object to be modified.
     * @return \Cake\ORM\RulesChecker
     */
    public function buildRules(RulesChecker $rules)
    {
        $rules->add($rules->existsIn(['user_id'], 'Users'));
        $rules->add($rules->existsIn(['role_id'], 'Roles'));

        return $rules;
    }
}

hm wierd, try to change
Users->find() to Users->findById($userId) and give $userId some id from database

That seems to have worked, but now I’m getting Unknown method "hasRight".
in Entity/User.php, I do have this code:

<?php
namespace App\Model\Entity;

use Cake\ORM\Entity;
use Cake\Auth\DefaultPasswordHasher;

/**
 * User Entity
 *
 * @property int $id
 * @property string $username
 * @property string $email
 * @property string $password
 * @property \Cake\I18n\FrozenTime $created
 *
 * @property \App\Model\Entity\Article[] $articles
 * @property \App\Model\Entity\UsersRole[] $users_roles
 * @property \App\Model\Entity\UsersDetail $users_detail
 */
class User extends Entity
{

    /**
     * Fields that can be mass assigned using newEntity() or patchEntity().
     *
     * Note that when '*' is set to true, this allows all unspecified fields to
     * be mass assigned. For security purposes, it is advised to set '*' to false
     * (or remove it), and explicitly make individual fields accessible as needed.
     *
     * @var array
     */
    protected $_accessible = [
        'username' => true,
        'email' => true,
        'password' => true,
        'created' => true,
        'articles' => true,
        'users_roles' => true,
        'users_detail' => true
    ];

    /**
     * Fields that are excluded from JSON versions of the entity.
     *
     * @var array
     */
    protected $_hidden = [
        'password'
    ];

    protected function _setPassword($password) {
      if (strlen($password) > 0) {
        return (new DefaultPasswordHasher)->hash($password);
      }
    }
    
    public function hasRight(string $right){
      return collection($this->user_roles)->firstMatch([
        '{*}.roles.{*}.permissions.' . $right => 1   
      ]) !== null;
    }
}

EDIT:
This is my AdminController.php at this moment:

<?php
namespace App\Controller;

class AdminController extends AppController {
  public function initialize(){
    parent::initialize();
    $this->layout = 'admin';

    $this->loadModel('UsersRoles');
    $this->loadModel('Users');
  }

  public function index() {
    $user_roles = $this->Users->findById($this->Auth->user('id'))->contain(['UsersRoles' => ['Roles' => ['Permissions']]]);
    var_dump($user_roles->hasRight("access_cms"));
  }
}

you need to execute query by using ->first() ->toArray() and few more i dont remember out of my head :slight_smile: in the $user_roles add ->first() at the end, Also get your naming stright, you are not getting user_roles but user.

when I add ->first()->toArray(), I get the error (Cannot match provided foreignKey) again :\

also, I have corrected the naming (by changing $user_roles = ... to $user = ...)