如何在Zend Framework 2中为变量值构建ACL断言?

时间:2016-05-18 11:56:38

标签: php zend-framework2 authorization assertions zend-acl

我在acl.global.php中配置了一个简单的ACL,如下所示:

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
                ]
            ]
        ],
    ]
];

解析器遍历配置,并从数组元素生成$this->allow($role, $controller, $action);之类的Zend\Permissions\Acl#allow(...)调用。

现在我还需要限制用户对项目的单一视图(mydomain.tld/item/:id)的访问权限。如果用户的id等于item.user_id(表示:用户是作者/所有者),则用户应该只能获得访问权限。

我认为实现此要求的方法是扩展配置

'Item\Controller\Process' => [
    'index' => 'member',
    'create' => 'member',
    'showItem' => [
        'role' => 'member',
        'assertion' => 'UserIsOwner'
    ]
    'showList' => 'member'
]

并将Assertion注入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 === ???;
    }
}

但是现在我不知道,断言应该如何注入item.user_iddocu中的示例没有此问题,因为它资产于$_SERVER['REMOTE_ADDR']

我可以注入ItemService来查找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;
}

虽然我仍然需要外部数据 - 当前item.id

在什么地方可以/应该将变量项的数据(在这种情况下是item.user_iditem.id)注入断言?

2 个答案:

答案 0 :(得分:2)

最后,我通过resource注入变量数据来解决问题。不要认为,这是最干净或推荐的解决方案。无论如何它是有效的。但是知道如何以干净/更优雅的方式解决它会很好。

<强> 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;
    }
}

答案 1 :(得分:0)

我不知道您是否可以应用我的解决方案,因为我在包含Zend \ Permission \ Acl的AclService类中配置我的Acl。

在这个AclService中我定义了一个$ assertions变量,它是一个存储我必须使用的每个断言的对象的数组。

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;
    }
    }

}

什么时候检查某人是否被允许使用资源,我将注入在AclService中匹配的路由的参数,这些参数在先前实例化的每个断言类(函数'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);

    }

}

通过这种方式,您可以在断言类中访问这些参数。

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;
    }

}

如果您不能直接应用此解决方案,我希望它可以指向正确的方向。