3
votes

I'am using CakePhp3 for my website and I have to inject some custom validation logic based on the current user Id when I'am creating or modifying an entity.

The basic case is "Is the user allow to change this field to this new value" ? If' not, I want to raise a validation error (or an unauthorized exception).

In cakephp, for what I'am understanding, most of the application and businness rules must be placed on Models or 'ModelsTable'of the ORM. But, in this classes, the AuthComponent or the current session is not available.

I don't want to call manually a method on the entity from the controller each time I need to check. I would like to use a validator, something like :

$validator->add('protected_data', 'valid', [
            'rule' => 'canChangeProtectedData',
            'message' => __('You're not able to change this data !'),
            'provider' => 'table',
        ]);

Method on ModelTable :

public function canChangeProtectedData($value, array $context)
{
    \Cake\Log\Log::debug("canChangeProtectedData");
    // Find logged user, look at the new value, check if he is authorized to do that, return true/false
    return false;
}

I cakephp < 3, the AuthComponent have a static method 'AuthComponent::user()' that is not available anymore. So, how Can I do that in CakePhp 3 ?

Thank you for any response.

EDIT - Adding more details

So here are more details. In case of an REST API. I have an edit function of an entity. The "Article" Entity.

This Article has an owner with a foreign key on the column named "user_id" (nothing special here). My users are organized in groups with a leader on the group. Leaders of groups can change article's owner but "basics" users can't do it (but they can edit their own articles). Admin users can edit everything. So the edit method must be available for any authenticated user, but changing the "user_id" of the entity must be allowed and checked depending the case (if I'am admin yes, if I'am leader yes only if the new Id is one of my group and if I'am basic user no).

I can do this check on the controller but if I want this rule to be checked everywhere in my code where an Article is modified (in another method than the "Edit" of ArticlesController). So for me the Model seems the good place to put it no?

2
Ok... But, I don't want to "protect" all the "edit" method/action of my entity on the controller using mechanism describe in "Authorization" parts of the documentation. (// Deny one action $this->Auth->deny('edit')). And I want to ensure the user rights to do this change from anywhere in my controller's actions... How can I do ? (Maybe I'am missing something...) - Ben
@Ben Provide us with more details about what you exactly want to do (what model, what fields, what values and why the user cannot change it) and maybe we'll be able to give you some advice on how you could improve your design. - Holt
@AD7six I can accept that my desing is wrong. But I can't find a better one for now. And I'am here to understand how I can do better... I edit the question to add more details about the use case. - Ben
It sounds like you're still in some permutation of "I'm justified in doing what I'm doing, this is the right solution for now". Auth and validation aren't the same thing. Deciding whether a request is allowed to continue or not is controller logic not model logic. Authorization is a standard thing - you don't need to reinvent it, use the tools already at your disposal (the Auth component) configured however you need for your app. Good luck. - AD7six
This is a great question in my opinion. I have a similar question as @Ben's. I have a role field in my database. 1 is for admins 2 is for editors etc... If someone went into inspect element and added an input with name of role and made the value 1 they would effectively add themselves as an admin. I need to be able to validate for that specific field and make sure the user is logged in and already an admin before they can add someone else as an admin or any role for that matter. - Battousai

2 Answers

3
votes

Authentication vs Authorisation

  • Authentication means identifying an user by credentials, which most of the time boils down to "Is a user logged in".
  • Authorisation means to check if an user is allowed to do a specific action

So don't mix these two.

You don't want validation you want application rules

Taken from the book:

Validation vs. Application Rules

The CakePHP ORM is unique in that it uses a two-layered approach to validation.

The first layer is validation. Validation rules are intended to operate in a stateless way. They are best leveraged to ensure that the shape, data types and format of data is correct.

The second layer is application rules. Application rules are best leveraged to check stateful properties of your entities. For example, validation rules could ensure that an email address is valid, while an application rule could ensure that the email address is unique.

What you want to implement is complex application logic and more than just a simple validation, so the best way to implement this is as an application rule.

I'm taking a code snippet from one of my articles that explains a similar case. I had to check for a limitation of languages (translations) that can be associated to a model. You can read the whole article here http://florian-kraemer.net/2016/08/complex-application-rules-in-cakephp3/

<?php
namespace App\Model\Rule;

use Cake\Datasource\EntityInterface;
use Cake\ORM\TableRegistry;
use RuntimeException;

class ProfileLanguageLimitRule {

   /**
    * Performs the check
    *
    * @link http://php.net/manual/en/language.oop5.magic.php
    * @param \Cake\Datasource\EntityInterface $entity Entity.
    * @param array $options Options.
    * @return bool
    */
   public function __invoke(EntityInterface $entity, array $options) {
      if (!isset($entity->profile_constraint->amount_of_languages)) {
         if (!isset($entity->profile_constraint_id)) {
            throw new RuntimeException('Profile Constraint ID is missing!');
         }
         $languageLimit = $this->_getConstraintFromDB($entity);
      } else {
         $languageLimit = $entity->profile_constraint->amount_of_languages;
      }

      // Unlimited languages are represented by -1
      if ($languageLimit === -1) {
         return true;
      }

      // -1 Here because the language_id of the profiles table already counts as one language
      // So it's always -1 of the constraint value
      $count = count($entity->languages);
      return $count <= ($languageLimit - 1);
   }

   /**
    * Gets the limitation from the ProfileConstraints Table object.
    *
    * @param \Cake\Datasource\EntityInterface $entity Entity.
    * @return int
    */
   protected function _getConstraintFromDB(EntityInterface $entity) {
      $constraintsTable = TableRegistry::get('ProfileConstraints');
      $constraint = $constraintsTable->find()
         ->where([
            'id' => $entity['profile_constraint_id']
         ])
         ->select([
            'amount_of_languages'
         ])
         ->firstOrFail();

      return $constraint->amount_of_languages;
   }

}

I think it is pretty self-explaining. Make sure your entities user_id field is not accessible for the "public". Before saving the data, just after the patching add it:

$entity->set('user_id', $this->Auth->user('id'));

If you alter the above snippet and change the profile_constraint_id to user_id or whatever else you have there this should do the job for you.

What you really want is row / field level based authorisation

Guess you can use ACL for that, but I've never ever had the need for field based ACL yet. So I can't give you much input on that, but it was (Cake2) and still is (Cake3) possible. For Cake3 the ACL stuff was moved to a plugin. Technically it is possible to check against anything, DB fields, rows, anything.

You could write a behavior that uses the Model.beforeMarshal event and checks if user_id (or role, or whatever) is present and not empty and then run a check on all fields you want for the given user id or user role using ACL.

You could probably use this method PermissionsTable::check() or you can write a more dedicated method does checks on multiple objects (fields) at the same time. Like I said, you'll spend some time to figure the best way out using ACL if you go for it.

UX and yet another cheap solution

First I would not show fields at all an user is not allowed to change or enter as inputs. If you need to show them, fine, disable the form input or just show it as text. Then use a regular set of validation rules that requires the field to be empty (or not present) or empty a list of fields based on your users role. If you don't show the fields the user would have to temper the form and then fail the CSRF check as well (if used).

1
votes

I don't think you need to validate in the table. I just thought of a way to do it in the controller.

In my Users/Add method in the controller for instance:

public function add()
{
    $user = $this->Users->newEntity();
    if ($this->request->is('post')) {
        $user = $this->Users->patchEntity($user, $this->request->data);

        //check if user is logged in and is a certain user
        if ($this->request->session()->read('Auth.User.id') === 1) {
            //allow adding/editing role or whatever
            $user->role = $this->request->data('role');
        } else {
            $user->role = 4;//or whatever the correct data is for your problem.
        }
        if ($this->Users->save($user)) {
            $this->Flash->success(__('You have been added.'));
        } else {
            $this->Flash->error(__('You could not be added. Please, try again.'));
        }
    }
    $this->set(compact('user'));
    $this->set('_serialize', ['user']);
}