问题
I have a simple ACL configures in an acl.global.php
like this:
return [
'acl' => [
'roles' => [
'guest' => null,
'member' => 'guest',
'admin' => 'member'
],
'resources' => [
'allow' => [
'Application\Controller\Index' => ['all' => 'member'],
'Application\Controller\Error' => ['all' => 'member'],
'Item\Controller\Process' => [
'index' => 'member',
'create' => 'member',
'showItem' => 'member', // website.tld/item/:id
'showList' => 'member' // website.tld/list-items
]
]
],
]
];
A parser iterates through the configuration and generates from the array elements calls to Zend\Permissions\Acl#allow(...) like $this->allow($role, $controller, $action);
.
Now I need additionally to restrict the access of the users to the item's single view (mydomain.tld/item/:id
). A user should only get the access, if its id
equals to the item.user_id
(means: the user is the author/owner).
The way I see to implement this requirement is to extend the config
'Item\Controller\Process' => [
'index' => 'member',
'create' => 'member',
'showItem' => [
'role' => 'member',
'assertion' => 'UserIsOwner'
]
'showList' => 'member'
]
and to inject the Assertion to Zend\Permissions\Acl#allow(...): $this->allow($role, $controller, $action, $assertion);
.
namespace Authorization\Acl\Assertion;
use ...
class UserIsOwner implements AssertionInterface
{
protected $userId;
// To inject the $userId can be the job of the factory.
public function __construct(int $userId)
{
$this->userId = $userId;
}
public function assert(Acl $acl, RoleInterface $role = null, ResourceInterface $resource = null, $privilege = null)
{
return return $this->userId === ???;
}
}
But now I have no idea, how the assertion should get the item.user_id
injected. The example in the docu doesn't have this problem, since it assets against the $_SERVER['REMOTE_ADDR']
.
I can inject the ItemService
to find out the item.user_id
:
public function assert(Acl $acl, RoleInterface $role = null, ResourceInterface $resource = null, $privilege = null)
{
return $this->isUserOwner();
}
protected function isUserOwner()
{
$itemId = ???;
$item = $this->itemService->findOne($itemId);
$itemOwnerId = $item->getUser()->getId();
return $this->userId == $itemOwnerId;
}
Though then I still need external data -- the current item.id
.
At what place can/should the variable item's data (in this case the item.user_id
or item.id
) be injected to an assertion?
回答1:
Finally I resolved the problem by injecting the variable data via the resource
. Don't think, that it's the cleanest or a recommended solution. Anyway it works. But it would be nice to know, how to resolve it a clean / more elegant way.
UserIsOwner
namespace Authorization\Acl\Assertion;
use Zend\Permissions\Acl\Assertion\AssertionInterface;
use Zend\Permissions\Acl\Acl;
use Zend\Permissions\Acl\Role\RoleInterface;
use Zend\Permissions\Acl\Resource\ResourceInterface;
use Item\Service\ItemService;
class UserIsOwner implements AssertionInterface
{
/**
*
* @var integer
*/
protected $userId;
/**
*
* @var ItemService
*/
protected $itemService;
public function __construct(int $userId, ItemService $itemService)
{
$this->userId = $userId;
$this->itemService = $itemService;
}
public function assert(Acl $acl, RoleInterface $role = null, ResourceInterface $resource = null, $privilege = null)
{
return isset($resource->getParams()['id']) ? $this->isUserOwner($resource->getParams()['id']) : false;
}
protected function isUserOwner($itemId)
{
$item = $this->itemService->findOne($itemId);
$itemOwnerId = $item->getUser()->getId();
return $this->userId == $itemOwnerId;
}
}
UserIsOwnerFactory
namespace Authorization\Acl\Assertion\Factory;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Authorization\Acl\Assertion\UserIsOwner;
class UserIsOwnerFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$itemFieldsetService = $serviceLocator->get('Item\Service\ItemService');
$authenticationService = $serviceLocator->get('AuthenticationService');
$userId = !empty($authenticationService->getIdentity()['id']) ? $authenticationService->getIdentity()['id'] : null;
$service = new UserIsOwner($userId, $itemFieldsetService);
return $service;
}
}
ParametrizedResource
namespace Authorization\Acl\Resource;
use Zend\Permissions\Acl\Resource\GenericResource;
use Zend\Mvc\Router\Http\RouteMatch;
class ParametrizedResource extends GenericResource
{
/**
* @var array Params. Here the RouteMatch#params.
* @see RouteMatch
*/
protected $params;
public function __construct($resourceId, array $params = [])
{
parent::__construct($resourceId);
$this->setParams($params);
}
/**
*
* @return the $params
*/
public function getParams()
{
return $this->params;
}
/**
*
* @param multitype: $params
*/
public function setParams($params)
{
$this->params = $params;
}
}
Acl
...
// @todo refactor
protected function addResources(array $resources)
{
foreach ($resources as $permission => $controllers) {
foreach ($controllers as $controller => $actions) {
if ($controller == 'all') {
$controller = null;
} else {
if (! $this->hasResource($controller)) {
$this->addResource(new Resource($controller, $this->routeMatchParams));
}
}
foreach ($actions as $action => $roleConfig) {
if (is_array($roleConfig)) {
foreach ($roleConfig as $role => $assertion) {
if ($action == 'all') {
$action = null;
}
$assertion = !empty($this->assertions[$assertion]) ? $this->assertions[$assertion] : null;
if ($permission == 'allow') {
$this->allow($role, $controller, $action, $assertion);
} elseif ($permission == 'deny') {
$this->deny($role, $controller, $action, $assertion);
} else {
throw new \Exception('No valid permission defined: ' . $permission);
}
}
} elseif (is_string($roleConfig)) {
if ($action == 'all') {
$action = null;
}
if ($permission == 'allow') {
$this->allow($roleConfig, $controller, $action);
} elseif ($permission == 'deny') {
$this->deny($roleConfig, $controller, $action);
} else {
throw new \Exception('No valid permission defined: ' . $permission);
}
}
}
}
}
return $this;
}
...
AclFactory
namespace Authorization\Acl\Factory;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Authorization\Acl\Acl;
class AclFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$config = $serviceLocator->get('Config');
$assertions = [
'UserIsOwner' => $serviceLocator->get('Assertion\UserIsOwner')
];
$routeMatch = $serviceLocator->get('Application')->getMvcEvent()->getRouteMatch();
$routeMatchParams = $routeMatch->getParams();
$service = new Acl($config, $assertions, $routeMatchParams);
return $service;
}
}
回答2:
I don't know if you can apply my solution because I configure my Acl in a AclService Class which wraps the Zend\Permission\Acl.
In this AclService I have defined a $assertions variable, which is an array which stores an object of every assertion that I have to use.
namespace User\Service;
use Zend\Permissions\Acl\Role\GenericRole as Role;
use Zend\Permissions\Acl\Resource\GenericResource as Resource;
use Zend\Permissions\Acl\Acl;
use User\Service\Assertion\RightLeagueAssertion;
use User\Service\Assertion\RightLeagueTeamAssertion;
class AclService {
const ROLE_GUEST = 'guest';
const ROLE_MEMBER = 'member';
const ROLE_COMISSIONER = 'comissioner';
const ROLE_ADMIN = 'admin';
const ROLE_GOD = 'god';
const ASSERTION_RIGHT_LEAGUE_TEAM = 'RightLeagueTeamAssertion';
protected $acl = null;
protected $assertions;
/**
* Constructor
*
* @param Acl $acl
* @return void
* @throws \Exception
*/
public function __construct($acl)
{
$this->acl = $acl;
$this->assertions[self::ASSERTION_RIGHT_LEAGUE_TEAM] = $rightLeagueTeam;
/* Declaramos los roles */
$this->acl->addRole(new Role(self::ROLE_GUEST));
$this->acl->addRole(new Role(self::ROLE_MEMBER), self::ROLE_GUEST);
$this->acl->addRole(new Role(self::ROLE_COMISSIONER), self::ROLE_MEMBER);
$this->acl->addRole(new Role(self::ROLE_ADMIN), self::ROLE_MEMBER);
//unique role for superadmin
$this->acl->addRole(new Role(self::ROLE_GOD));
/* Declaramos los recursos (module:controller) */
$this->acl->addResource(new Resource('application:index'));
$this->acl->addResource(new Resource('application:error'));
$this->acl->addResource(new Resource('user:user'));
$this->acl->addResource(new Resource('leueroneyear:league'));
$this->acl->addResource(new Resource('leueroneyear:team'));
/*** Permisos ***/
//'God' tiene permiso para todo
$this->acl->allow(self::ROLE_GOD);
//Una persona no logueada podrá ver solo el índice, errores, darse de alta y recuperar el password
$this->acl->allow(self::ROLE_GUEST, 'application:index', 'index');
$this->acl->allow(self::ROLE_GUEST, 'user:user', array('register','forgotpassword','resetpassword','login'));
$this->acl->allow(self::ROLE_GUEST, 'application:error');
$this->acl->allow(self::ROLE_GUEST, 'nba:test');
//Los usuarios sí que podrán visitar las páginas
$this->acl->allow(self::ROLE_MEMBER, 'user:user', array('get','edit', 'logout'));
$this->acl->allow(self::ROLE_MEMBER, 'leueroneyear:league', array('index','get','list','add','enter'));
$this->acl->allow(self::ROLE_MEMBER, 'leueroneyear:team', array('get','add'));
$this->acl->allow(self::ROLE_MEMBER, 'leueroneyear:team', 'index',$this->assertions[self::ASSERTION_RIGHT_LEAGUE_TEAM]);
}
public function getAcl()
{
return $this->acl;
}
public function isAllowed($role, $controller, $action)
{
$a = explode("\\",$controller);
$resource = strtolower($a[0]).":".strtolower($a[2]);
//\Zend\Debug\Debug::dump($resource); die();
return $this->acl->isAllowed($role, $resource, $action);
}
public function setRequestParams($params)
{
$a = explode("\\",$params["controller"]);
$controller = strtolower($a[2]);
switch ($controller) {
case 'team': $this->assertions[self::ASSERTION_RIGHT_LEAGUE_TEAM]->setRequestParams($params);
break;
}
}
}
When is time to check if somebody is allowed to use a resource, I inject the parameters of the route matched in the AclService which injects them in every assertion class previously instantiated (function 'setRequestParams').
/**
* @param MvcEvent $e
*/
public function onRoute(MvcEvent $event)
{
$matches = $event->getRouteMatch();
$controller = $matches->getParam('controller');
$action = $matches->getParam('action','index');
$auth = $this->authService;
/* @var $user User\Entity\User */
if ($user = $auth->getIdentity()) {
$session = new Container("League");
if (isset($session->isCommissioner) && $session->isCommissioner)
$role = AclService::ROLE_COMISSIONER;
else
$role = AclService::ROLE_MEMBER;
} else {
$role = AclService::ROLE_GUEST;
}
$acl = $this->aclService;
$acl->setRequestParams($matches->getParams());
if (!$acl->isAllowed($role,$controller, $action)) {
//El usuario no tiene los permisos necesarios
$app = $event->getTarget();
$route = $event->getRouteMatch();
$event -> setError(RouteGuard::ERROR)
-> setParam('route', $route->getMatchedRouteName());
$app->getEventManager()->trigger('dispatch.error', $event);
}
}
In this way, you can access these parameters in your assertion classes.
class RightLeagueTeamAssertion implements AssertionInterface
{
protected $requestParams;
public function setRequestParams($params)
{
$this->requestParams = $params;
}
/**
* Comprueba que el idTeam que pasan por parámetro pertenece a la liga en la que estás logueado
*/
public function assert(Acl $acl, RoleInterface $role = null, ResourceInterface $resource = null, $privilege = null) {
$appSession = new Container("Application");
$leagueSession = new Container("League");
$idLeague = $leagueSession->idLeague;
$idTeam = $this->requestParams['id'];
\Zend\Debug\Debug::dump($idTeam);
return false;
}
}
If you can not apply this solution directly, I hope it can point you to the right direction.
来源:https://stackoverflow.com/questions/37298894/how-to-build-an-acl-assertion-for-a-variable-value-in-zend-framework-2