CakePHP 4.1 User entity as authorization identity associated fields

ぐ巨炮叔叔 提交于 2020-08-10 13:01:25

问题


I have just created a very minimal project in CakePHP 4.1, mostly mimicking the CMS tutorial, and want to implement a fairly straightforward piece of logic. Using the Authorization module I want to allow a user A to be able to view a user B if 1) they are actually the same user (A = B) OR 2) if A is an admin.

There are two DB tables - users and user_types. users has a foreign key user_type_id to user_types.

This relationship is reflected in code as:

##### in UsersTable.php #####

class UsersTable extends Table {
    public function initialize(array $config): void
    {
        parent::initialize($config);

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

        $this->addBehavior('Timestamp');
    }

    //...
}



##### in UserTypesTable.php #####

class UserTypesTable extends Table {
    public function initialize(array $config): void
    {
        parent::initialize($config);

        $this->setTable('user_types');
        $this->setDisplayField('name');
        $this->setPrimaryKey('id');
        $this->hasMany('Users');
    }

    //...
}

In UsersController.php I have:

    public function view($id = null)
    {
        $user = $this->Users->get($id, [
            'contain' => ['UserTypes'],
        ]);

        $this->Authorization->authorize($user);

        $this->set(compact('user'));
    }

And in UserPolicy.php:

use App\Model\Entity\User;

class UserPolicy
{
    public function canView(User $user, User $resource)
    {
        // TODO: allow view if $user and $resource are the same User or if $user is an admin
        //
        // My problem is that here $user->user_type is NULL
        // while $resource->user_type is populated correctly
    }
}

The code comment in the above excerpt shows where my problem is. I do not know how to get $user to have its user_type field populated in order to check whether they're an admin.

As a part of my efforts, I have set the User class to be the authorization identity, following this article: https://book.cakephp.org/authorization/2/en/middleware.html#using-your-user-class-as-the-identity. Code-wise this looks like:

##### relevant part of Application.php #####

$middlewareQueue
            ->add(new AuthenticationMiddleware($this))
            ->add(new AuthorizationMiddleware($this, [
                'identityDecorator' => function(\Authorization\AuthorizationServiceInterface $auth, \Authentication\IdentityInterface $user) {
                    return $user->getOriginalData()->setAuthorization($auth);
                }
            ]));





##### User.php #####

namespace App\Model\Entity;

use Authentication\PasswordHasher\DefaultPasswordHasher;
use Authorization\AuthorizationServiceInterface;
use Authorization\Policy\ResultInterface;
use Cake\ORM\Entity;

/**
 * User Entity
 *
 * @property int $id
 * @property string $email
 * @property string $password
 * @property string|null $name
 * @property \App\Model\Entity\UserType $user_type
 * @property \Cake\I18n\FrozenTime|null $created
 * @property \Cake\I18n\FrozenTime|null $modified
 * @property \Authorization\AuthorizationServiceInterface $authorization
 */
class User extends Entity implements \Authorization\IdentityInterface, \Authentication\IdentityInterface
{
    protected $_accessible = [
        'email' => true,
        'password' => true,
        'name' => true,
        'created' => true,
        'modified' => true,
    ];

    /**

    protected $_hidden = [
        'password',
    ];

    protected function _setPassword(string $password) : ?string
    {
        if (strlen($password) > 0) {
            return (new DefaultPasswordHasher())->hash($password);
        }
    }

    /**
     * @inheritDoc
     */
    public function can(string $action, $resource): bool
    {
        return $this->authorization->can($this, $action, $resource);
    }

    /**
     * @inheritDoc
     */
    public function canResult(string $action, $resource): ResultInterface
    {
        return $this->authorization->canResult($this, $action, $resource);
    }

    /**
     * @inheritDoc
     */
    public function applyScope(string $action, $resource)
    {
        return $this->authorization->applyScope($this, $action, $resource);
    }

    /**
     * @inheritDoc
     */
    public function getOriginalData()
    {
        return $this;
    }

    /**
     * Setter to be used by the middleware.
     * @param AuthorizationServiceInterface $service
     * @return User
     */
    public function setAuthorization(AuthorizationServiceInterface $service)
    {
        $this->authorization = $service;

        return $this;
    }

    /**
     * @inheritDoc
     */
    public function getIdentifier()
    {
        return $this->id;
    }
}

However, I have not been able to get the identity User in the UserPolicy.php file to have the user_type field populated. Some under-the-hood magic seems to happen when I call $this->Authorization->authorize() from the controller where I explicitly pass the resource together with its user type (since I have constructed it with 'contain' => ['UserTypes'] BUT the identity user is populated automatically by the Authorization module. Could someone please help me to find a way to bring associated tables data into the identity user of an authorization policy?

NOTE: I have fudged the code to make it work like this:

##### in UserPolicy.php #####


use App\Model\Entity\User;

class UserPolicy
{
    public function canView(User $user, User $resource)
    {
        $user = \Cake\Datasource\FactoryLocator::get('Table')->get('Users')->get($user->id, ['contain' => ['UserTypes']]);

        // Now both $user->user_type and $resource->user_type are correctly populated
    }
}

HOWEVER, this feels awfully "hacky" and not the way it's supposed to be, so my original question still stands.


回答1:


The identity is being obtained by the resolver of the involved identifier. In case of the CMS tutorial that's the Password identifier which by default uses the ORM resolver.

The ORM resolver can be configured to use a custom finder in case you need to control the query for obtaining the user, that's where you should add the containment for your UserTypes association.

In your UsersTable add a finder like this:

public function findForAuthentication(\Cake\ORM\Query $query, array $options): \Cake\ORM\Query
{
    return $query->contain('UserTypes');
}

and configure the identifier's resolver to use that finder like this:

$service->loadIdentifier('Authentication.Password', [
    'resolver' => [
        'className' => 'Authentication.Orm',
        'finder' => 'forAuthentication',
    ],
    'fields' => [
        'username' => 'email',
        'password' => 'password',
    ]
]);

You need to specify the resolver class name too when overriding the resolver option, as by default it is just a string, not an array that would merge with the new config!

See also

  • Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Custom Finder Methods
  • Authentication Cookbook > Identifiers


来源:https://stackoverflow.com/questions/63057543/cakephp-4-1-user-entity-as-authorization-identity-associated-fields

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!