问题
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