Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
86.67% covered (success)
86.67%
13 / 15
CRAP
93.88% covered (success)
93.88%
92 / 98
AclUser\Permissions\Acl\Acl
0.00% covered (danger)
0.00%
0 / 1
86.67% covered (success)
86.67%
13 / 15
43.42
93.88% covered (success)
93.88%
92 / 98
 defineRoles
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
8 / 8
 defineResources
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
 joinAclToNavigation
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 checkAddParentRole
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
9 / 9
 getPresentUser
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 getPresentUserId
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 getPresentUserEmailAddress
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
5 / 5
 addPresentUserAgrigateRole
0.00% covered (danger)
0.00%
0 / 1
3.10
77.78% covered (success)
77.78%
7 / 9
 setAgragateRolesByUserId
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
10 / 10
 addMemRole
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 onDispatch
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
7 / 7
 disallowedRouteRedirect
0.00% covered (danger)
0.00%
0 / 1
10.60
81.82% covered (success)
81.82%
18 / 22
 checkThatRouteExists
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
7 / 7
 isParticularRoute
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
 userIsAllowed
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
<?php
/**
 * Class Acl
 *
 * @package     AclUser\Permissions\Acl
 * @author      Nigel Hurnell
 * @version     v.1.0.0
 * @license     BSD
 * @copyright   Copyright (c) 2017, Nigel Hurnell
 */
namespace AclUser\Permissions\Acl;
use Zend\Mvc\MvcEvent;
use Zend\Permissions\Acl\Acl as ZendAcl;
use Zend\View\Helper\Navigation;
use Zend\Router\Http\RouteMatch;
/**
 * Class Acl
 * 
 * Extends Zend\Acl
 * 
 * @package     AclUser\Permissions\Acl
 * @author      Nigel Hurnell
 * @version     v.1.0.0
 * @license     BSD
 * @copyright   Copyright (c) 2017, Nigel Hurnell
 */
class Acl extends ZendAcl
{
    /**
     * An array that stores the role name under the role's id as key
     * 
     * @var array
     */
    protected $roleMemory = [];
    /**
     * Array (strings) of all roles that exist in database
     *  
     * @var array 
     */
    protected $allRoles;
    /**
     * Configuration array 
     * 
     * @var array
     */
    protected $config;
    /**
     * Authentication and Identity management helper
     * 
     * @var Zend\Authentication\AuthenticationService 
     */
    protected $authService;
    /**
     * UserManager that handles logic for a registered User
     * 
     * @var AclUser\Service\UserManager
     */
    protected $userManager;
    /**
     * add all roles used by the application to the access control list parent object
     */
    protected function defineRoles()
    {
        $this->addRole('guest');
        $this->allRoles = ['guest'];
        $roles = $this->userManager->fetchAllRoles();
        foreach ($roles as $role) {
            $this->allRoles[] = $role->getName();
            $this->checkAddParentRole($role);
        }
        $this->addPresentUserAgrigateRole();
    }
    /**
     * add all resources used by the application to the access control list parent object
     * 
     * @param array $resources array of (string) resources 
     */
    protected function defineResources($resources)
    {
        foreach ($resources as $resource) {
            $this->addResource($resource);
        }
    }
    /**
     * Set user role and this ACL for navigation
     */
    protected function joinAclToNavigation()
    {
        Navigation::setDefaultAcl($this);
        Navigation::setDefaultRole('present_unique_user');
    }
    /**
     * Check whether role and parent role has been added to this ACL and store 
     * role so that it is not added a second time
     * 
     * @param AclUser\Entity\Role  $role
     */
    protected function checkAddParentRole($role)
    {
        $parent = $role->getParent();
        if ($parent) {
            $this->checkAddParentRole($parent);
            if (!$this->hasRole($role->getName())) {
                $this->addMemRole($role, $parent->getName());
            } else {
                
            }
        } else {
            if (!$this->hasRole($role->getName())) {
                $this->addMemRole($role);
            }
        }
    }
    /**
     * Get the User entity object that represents the present (logged in) user
     * 
     * @return null|User the logged in user
     */
    public function getPresentUser()
    {
        $user = null;
        if (null !== ($identity = $this->getPresentUserId())) {
            $user = $this->userManager->getUserById($identity);
        }
        return $user;
    }
    /**
     * Get the user id (auth service identity) the present (logged in) user
     * 
     * @return integer|null the present user id
     */
    public function getPresentUserId()
    {
        $identity = null;
        if ($this->authService->hasIdentity()) {
            $identity = $this->authService->getIdentity();
        }
        return $identity;
    }
    /**
     * Get the logged in user's e-mail address
     * 
     * @return string the logged in user's email address or "Identity" if user is not found
     */
    public function getPresentUserEmailAddress()
    {
        $result = 'Identity';
        $user = $this->getPresentUser();
        if ($user) {
            $result = $user->getEmail();
        }
        return $result;
    }
    /**
     * add the user role 'present_unique_user' that is used for all users 
     * And add the present users roles array as parent of this role
     */
    protected function addPresentUserAgrigateRole()
    {
        if (!$this->authService->hasIdentity()) {
            $this->addRole('present_unique_user', 'guest');
        } else {
            $userId = $this->authService->getIdentity();
            $user = $this->userManager->getUserById($userId);
            if ($user) {
                $this->setAgragateRolesByUserId($user);
            } else {
                $this->authService->clearIdentity();
                $this->addRole('present_unique_user', 'guest');
            }
        }
    }
    /**
     * Concatenate all users roles and add present_unique_user as child of all those roles
     * 
     * @param Entity\User $user
     */
    protected function setAgragateRolesByUserId($user)
    {
        $maps = $user->getRoleMaps();
        $userRoles = [];
        if (count($maps) == 0) {
            $userRoles[] = 'guest';
        }
        foreach ($maps as $map) {
            $role = $map->getRole();
            if (array_key_exists($role->getId(), $this->roleMemory) && !in_array($role->getName(), $userRoles)) {
                $userRoles[] = $this->roleMemory[$role->getId()];
            }
        }
        $this->addRole('present_unique_user', $userRoles);
    }
    /**
     * Persist roles for later use within this class
     * 
     * @param AclUser\Entity\Role $role
     * @param string|null $parentId
     */
    protected function addMemRole($role, $parentId = NULL)
    {
        $this->addRole($role->getName(), $parentId);
        $this->roleMemory[$role->getId()] = $role->getName();
    }
    /**
     * Based on ListenerAggregateInterface this method is called when application dispatches
     * 
     * @param MvcEvent $event
     * @return null
     */
    public function onDispatch(MvcEvent $event)
    {
        $routeMatch = $event->getRouteMatch();
        $resource = $routeMatch->getParam('controller');
        $privilege = $routeMatch->getParam('action');
        $allowed = $this->userIsAllowed($resource, $privilege);
        if ($allowed) {
            return;
        }
        return $this->disallowedRouteRedirect($event, $routeMatch);
    }
    /**
     * Handle request if user is not allowed the access the the requested resource and privilege
     * 
     * @param MvcEvent $event
     * @param RouteMatch $routeMatch
     * @return type
     */
    public function disallowedRouteRedirect(MvcEvent $event, RouteMatch $routeMatch)
    {
        $controller = $event->getTarget();
        switch (true) {
            case (!$this->checkThatRouteExists($routeMatch)):
                /* route does not exist */
                $controller->plugin('FlashMessenger')->setNamespace('error')->addMessage('The page that you requested does not exist.');
                return $controller->redirect()->toRoute('default', ['controller' => 'index', 'action' => 'index']);
            case $this->authService->hasIdentity() && $this->isParticularRoute($routeMatch, 'user-auth', 'login'):
                /* route exists but user is not permitted */
                $controller->plugin('FlashMessenger')->setNamespace('error')->addMessage('You tried to log in but you are already logged in.');
                return $controller->redirect()->toRoute('default', ['controller' => 'index', 'action' => 'index']);
            case $this->authService->hasIdentity():
                /* route exists but user is not permitted */
                $controller->plugin('FlashMessenger')->setNamespace('error')->addMessage('You do not have permission to visit the requested page.');
                return $controller->redirect()->toRoute('default', ['controller' => 'index', 'action' => 'index']);
            case (!$this->isParticularRoute($routeMatch, 'user-auth', 'login') && $this->userIsAllowed('user-auth', 'login')):
                if ($this->isParticularRoute($routeMatch, 'user-auth', 'logout')) {
                    /* in case user who is not logged in tries to log out */
                    $controller->plugin('FlashMessenger')->setNamespace('error')->addMessage('You tried to log out but you are not logged in.');
                    return $controller->redirect()->toRoute('default', ['controller' => 'user-auth', 'action' => 'login']);
                }
                /* route exists - user is not logged in so redirect to login with redirect url so that they can maybe login and then access  */
                $uri = $event->getApplication()->getRequest()->getUri();
                // Make the URL relative (remove scheme, user info, host name and port)
                // to avoid redirecting to other domain by a malicious user.
                $uri->setScheme(null)->setHost(null)->setPort(null)->setUserInfo(null);
                $controller->plugin('FlashMessenger')->setNamespace('error')->addMessage('You do not have permission to visit the requested page.');
                return $controller->redirect()->toRoute('default', ['controller' => 'user-auth', 'action' => 'login'], ['query' => ['redirectUrl' => $uri->toString()]]);
            case (!$this->isParticularRoute($routeMatch, 'user-auth', 'logout') && $this->userIsAllowed('user-auth', 'logout')):
                return $controller->redirect()->toRoute('default', ['controller' => 'user-auth', 'action' => 'logout']);
            default:
                $controller->plugin('FlashMessenger')->setNamespace('error')->addMessage('Something went wrong.');
                return $controller->redirect()->toRoute('default', ['controller' => 'index', 'action' => 'index']);
        }
    }
    /**
     * Check whether this route corresponds to a real controller and action
     * 
     * @param RouteMatch $routeMatch
     */
    private function checkThatRouteExists(RouteMatch $routeMatch)
    {
        $controller = $routeMatch->getParam('controller');
        if (!$this->hasResource($controller)) {
            //note that this cannot fail because The requested controller could not be mapped to an existing controller class. would have been called
        }
        $action = $routeMatch->getParam('action');
        foreach ($this->allRoles as $role) {
            if ($this->isAllowed($role, $controller, $action)) {
                return true;
            }
        }
        return false;
    }
    /**
     * Check whether this is particular route as defined by the controller and the action
     * 
     * @param RouteMatch $routeMatch
     * @param string $controller
     * @param string $action
     * @return boolean
     */
    private function isParticularRoute(RouteMatch $routeMatch, $controller, $action)
    {
        $isController = $routeMatch->getParam('controller') == $controller;
        $isAction = $routeMatch->getParam('action') == $action;
        return $isController && $isAction;
    }
    /**
     * Check whether current user is allowed to access this resource and privilege 
     * 
     * @param string $resource
     * @param string $privilege
     * @return boolean whether user is permitted to access this route
     */
    public function userIsAllowed($resource, $privilege)
    {
        return $this->isAllowed('present_unique_user', $resource, $privilege);
    }
}