diff --git a/app/config/acl.yml b/app/config/acl.yml new file mode 100644 index 0000000..0f27795 --- /dev/null +++ b/app/config/acl.yml @@ -0,0 +1,42 @@ + +# ACL in this system is defined as follows: +# +# - Roles: +# Roles define a group of user. like Author, Admin, Guest etc. +# Each role can inherit other roles with the "inherit" key. +# Each role can gain access to a zone (explained later) by the +# "allowed-zones" key. Per default a role is denied access to all zones. +# +# - Resources: +# Resources maps directly to controller names. +# There a 2 controllers/resources that are a bit special, +# index and error resources are always accessible by everyone (e.g. they +# are not part of the ACL). +# +# - Access levels. +# These are not used in this system. a hardcoded "All" level is used. +# +# Zones +# +# Zones defines a group of resources. for example an "backend" zone can +# have 2 controllers/resources (site-config, user-manager) +# +# Zones might be implemented using modules later. + +acl: + roles: + guest: + allowed-zones: public + description: Non logged in users + user: + inherits: guest + allowed-zones: user + description: Logged in users + #admin: + # inherits: user + # allowed-zones: backend + + zones: + public: [ auth, api ] + user: [ user, callback ] + #backend: [ site, user-man ] diff --git a/app/config/routes.yml b/app/config/routes.yml index 4c36381..7b0f625 100644 --- a/app/config/routes.yml +++ b/app/config/routes.yml @@ -20,7 +20,7 @@ router: cb-endpoint: pattern: '/cb/{id}/:params' path: - controller: callback + controller: api action: endpoint login: pattern: '/login' @@ -54,5 +54,5 @@ router: activation-link: pattern: '/activate/{link}' path: - controller: user + controller: api action: activationlink diff --git a/app/controllers/ApiController.php b/app/controllers/ApiController.php new file mode 100644 index 0000000..50a79a8 --- /dev/null +++ b/app/controllers/ApiController.php @@ -0,0 +1,90 @@ +view->disable(); + + $allowed_methods = array('GET', 'POST'); + if ($this->request->isMethod($allowed_methods)) { + + $callback = CallbackModel::get($id); + + $request = new RequestModel(); + + $request->setHeaders($this->request->getHeaders()); + $request->setBody($this->request->getRawBody()); + + $dt = new \DateTime(); + + $callback->setLastRequest($dt->format('Y-m-d H:i:s')); + + $meta = new RequestMetaModel(); + $meta->Callback = $callback; + $meta->RequestObject = $request; + + $meta->setSourceIp($this->request->getClientAddress()); + $meta->setMethod($this->request->isPost() ? 'POST' : 'GET'); + $meta->setUri($this->request->getServer('REQUEST_URI')); + + $result = $meta->save(); + if ($result == false) { + var_dump($meta->getMessages()); + } + } + } + + /** + * Account/Password activation. + * + * @param $id + */ + public function activationLinkAction($id) + { + $link = UserActivation::findFirst(['activation_key = ?0', 'bind' => [ $id ]]); + + if ($link) { + if ($link->isValid()) { + + $user = $link->getUser(); + + // Save password if any is set. + if (strlen($link->getPassword()) > 0) { + $user->setPassword($link->getPassword()); + $this->flash->success('Your password has been activated.'); + } else { + $user->setStatus(User::STATUS_ACTIVE); + $this->flash->success('Your account has been activated.'); + + // Also login the user. + $this->auth->systemLogin($user); + } + $user->save(); + } else { + $this->flash->error('This link has expired or has already been used.'); + } + + // Make sure the link is deleted. + $link->delete(); + } else { + $this->flash->error('This does not seem to be an active link'); + } + } +} diff --git a/app/controllers/CallbackController.php b/app/controllers/CallbackController.php index d36e597..64354d1 100644 --- a/app/controllers/CallbackController.php +++ b/app/controllers/CallbackController.php @@ -4,9 +4,7 @@ namespace App\Controller; use App\Controller\ControllerBase, App\Form\CallbackCreate as CreateCallbackForm, - App\Model\Data\Callback as CallbackModel, - App\Model\Data\Request as RequestModel, - App\Model\Data\RequestMeta as RequestMetaModel; + App\Model\Data\Callback as CallbackModel; class CallbackController extends ControllerBase { @@ -116,48 +114,4 @@ class CallbackController extends ControllerBase $this->view->page = $paginator->getPaginate(); $this->view->pagination_url = '/callback/show/' . $id . '/'; } - - /** - * This is the action that the API to be - * tested should make it's callback to. So we can catch it. - * - * @Acl(resource="api") - * @param int $id The test session id so - * we know what test it belongs to. - * @return string - */ - public function endpointAction($id) - { - $this->view->disable(); - - $allowed_methods = array('GET', 'POST'); - if ($this->request->isMethod($allowed_methods)) { - - $callback = CallbackModel::get($id); - - $request = new RequestModel(); - - $request->setHeaders($this->request->getHeaders()); - $request->setBody($this->request->getRawBody()); - - $dt = new \DateTime(); - - $callback->setLastRequest($dt->format('Y-m-d H:i:s')); - - $meta = new RequestMetaModel(); - $meta->Callback = $callback; - $meta->RequestObject = $request; - - $meta->setSourceIp($this->request->getClientAddress()); - $meta->setMethod($this->request->isPost() ? 'POST' : 'GET'); - $meta->setUri($this->request->getServer('REQUEST_URI')); - - $result = $meta->save(); - if ($result == false) { - var_dump($meta->getMessages()); - } - } - - - } } diff --git a/app/controllers/UserController.php b/app/controllers/UserController.php index 3ce35fa..436f35e 100644 --- a/app/controllers/UserController.php +++ b/app/controllers/UserController.php @@ -106,44 +106,6 @@ class UserController extends ControllerBase $this->response->redirect('/settings'); } - /** - * Account/Password activation. - * - * @Acl(resource="auth") - * @param $id - */ - public function activationLinkAction($id) - { - $link = UserActivation::findFirst(['activation_key = ?0', 'bind' => [ $id ]]); - - if ($link) { - if ($link->isValid()) { - - $user = $link->getUser(); - - // Save password if any is set. - if (strlen($link->getPassword()) > 0) { - $user->setPassword($link->getPassword()); - $this->flash->success('Your password has been activated.'); - } else { - $user->setStatus(User::STATUS_ACTIVE); - $this->flash->success('Your account has been activated.'); - - // Also login the user. - $this->auth->systemLogin($user); - } - $user->save(); - } else { - $this->flash->error('This link has expired or has already been used.'); - } - - // Make sure the link is deleted. - $link->delete(); - } else { - $this->flash->error('This does not seem to be an active link'); - } - } - public function activityAction($page = 1) { $user = $this->_getAuth()->getUser(); diff --git a/app/library/Acl.php b/app/library/Acl.php index be6ed5d..80a3157 100644 --- a/app/library/Acl.php +++ b/app/library/Acl.php @@ -2,50 +2,93 @@ namespace Httpcb; -use Phalcon\Acl\Role, - Phalcon\Acl\Adapter\Memory as AclList; +use Phalcon\Config, + Phalcon\Acl\Role, + Phalcon\Acl\Adapter\Memory as Adapter; -class Acl extends AclList +class Acl { const ROLE_USER = 'user'; const ROLE_GUEST = 'guest'; - public function __construct() + /** + * @var Adapter + */ + protected $_adapter = null; + + public function __construct(Config $config) { + $this->_adapter = new Adapter(); + // Deny access to everything by default. - $this->setDefaultAction(\Phalcon\Acl::DENY); + $this->_adapter->setDefaultAction(\Phalcon\Acl::DENY); - // Roles - $guest = new Role(self::ROLE_GUEST); - $user = new Role(self::ROLE_USER); - - $this->addRole($guest); - $this->addRole($user, $guest); - - // Public Resources - $public = array( - 'index', - 'error', - 'auth', - 'api', - ); - - $this->_grant($guest, $public); - - // Protected Resources - $protected = array( - 'callback', - 'user', - ); - - $this->_grant($user, $protected); + $this->fromConfig($config); } - protected function _grant(Role $role, array $resources) + /** + * @param $role + * @param $resource + * @return bool + */ + public function isAllowed($role, $resource) { - foreach($resources as $resource) { - $this->addResource($resource, 'Read'); - $this->allow($role->getName(), $resource, 'Read'); + return $this->_adapter->isAllowed($role, $resource, 'All') == \Phalcon\Acl::ALLOW; + } + + /** + * @param string $resource + * @return bool + */ + public function hasResource($resource) + { + return $this->_adapter->isResource($resource); + } + + public function fromConfig(Config $config) + { + // Add roles. + foreach($config->roles as $name => $def) { + + $inherits = null; + $description = null; + + if ($def instanceof Config) { + $inherits = $def->get('inherits'); + $description = $def->get('description'); + + } + + $role = new Role($name, $description); + $this->_adapter->addRole($role, $inherits); + } + + // Zones + foreach($config->zones as $name => $resources) { + + if (!($resources instanceof Config)) { + $resources = new Config([ $resources ]); + } + + foreach($resources as $resource) { + $this->_adapter->addResource($resource, 'All'); + } + } + + // Grant access for roles and resources. + foreach($config->roles as $name => $def) { + + $zones = $def->get('allowed-zones', []); + + if (is_string($zones)) { + $zones = [ $zones ]; + } + + foreach($zones as $zone) { + foreach($config->zones->get($zone) as $resource) { + $this->_adapter->allow($name, $resource, 'All'); + } + } } } } diff --git a/app/library/Services.php b/app/library/Services.php index a1485fd..af06def 100644 --- a/app/library/Services.php +++ b/app/library/Services.php @@ -30,7 +30,7 @@ use Httpcb\Auth, Httpcb\Mail as MailService, Httpcb\ViewHelper\Volt\Extension as ViewHelperVoltExtension; -use App\Listener\AclListener, +use App\Listener\AccessListener, App\Listener\DispatchListener, App\Listener\ActivityLog; @@ -70,7 +70,8 @@ class Services extends DiDefault 'app.yml', 'routes.yml', 'menu.yml', - 'local.yml' + 'local.yml', + 'acl.yml' ); $basePath = APP_PATH . '/app/config/'; @@ -124,7 +125,7 @@ class Services extends DiDefault $eventsManager->attach('dispatch:beforeException', new DispatchListener()); } - $eventsManager->attach('dispatch:beforeExecuteRoute', new AclListener()); + $eventsManager->attach('dispatch:beforeExecuteRoute', new AccessListener()); $dispatcher = new \Phalcon\Mvc\Dispatcher(); $dispatcher->setEventsManager($eventsManager); @@ -351,7 +352,7 @@ class Services extends DiDefault protected function _initAcl() { - return new Acl(); + return new Acl($this->get('config')->acl); } protected function _initSharedLogger() diff --git a/app/listeners/AccessListener.php b/app/listeners/AccessListener.php new file mode 100644 index 0000000..92468e0 --- /dev/null +++ b/app/listeners/AccessListener.php @@ -0,0 +1,70 @@ +auth->hasIdentity()) { + $role = Acl::ROLE_USER; + } else { + $role = Acl::ROLE_GUEST; + } + + // Get the resource from controller name. + $resource = $dispatcher->getControllerName(); + + // Ignore checks for error resource. + if (in_array($resource, $this->_ignored_resources)) { + return true; + } + + // Now, check and redirect user to login page if + // this role does not have access to this resource. + if ($this->acl->isAllowed($role, $resource) === false) { + + // Has identity or acl_redirect flag set. + // Throw a "handler not found" exception in this case. + if ($this->auth->hasIdentity() || $this->session->has('acl_redirect')) { + + // Unset redirect flag first. + unset($this->session->acl_redirect); + + $msg = sprintf("Role '%s' not allowed access to resource '%s'", $role, $resource); + throw new DispatcherException($msg, Dispatcher::EXCEPTION_HANDLER_NOT_FOUND); + } + + // Redirect to login page + $this->response->redirect(['for' => 'login']); + + // And set a flag in session. if we do not have access to that + // resource either. we should not redirect again. + $this->session->set('acl_redirect', true); + + + // Return false to stop the dispatch loop. + return false; + } + return true; + } +} diff --git a/app/listeners/AclListener.php b/app/listeners/AclListener.php deleted file mode 100644 index cd8b732..0000000 --- a/app/listeners/AclListener.php +++ /dev/null @@ -1,53 +0,0 @@ -auth->hasIdentity()) { - $role = Acl::ROLE_USER; - } else { - $role = Acl::ROLE_GUEST; - } - - // Support annotations for actions to define custom resources. - $controllerClass = $dispatcher->getControllerClass(); - $activeMethod = $dispatcher->getActiveMethod(); - - $annotation = $this->annotations->getMethod($controllerClass, $activeMethod); - - // ACL annotation found. use that. - if ($annotation->has('Acl')) { - $resource = $annotation->get('Acl')->getArgument('resource'); - } - // Otherwise, default to controller name. - else { - $resource = $dispatcher->getControllerName(); - } - - // Now, check and redirect user to login page if - // this role does not have access to this resource. - if ($this->acl->isAllowed($role, $resource, 'Read') == \Phalcon\Acl::DENY) { - - // Forward to login page. - $dispatcher->forward(array( - 'controller' => 'auth', - 'action' => 'index', - )); - - // Return false to stop the dispatch loop. - return false; - } - - return true; - } -}