Archived
1
0
Fork 0

Merge branch '26-acl' into 'dev'

Resolve "Acl"

Closes #26

See merge request pnx/httpcb!24
This commit is contained in:
Henrik Hautakoski 2018-10-01 14:08:44 +00:00
commit 52f17efed8
9 changed files with 286 additions and 177 deletions

42
app/config/acl.yml Normal file
View file

@ -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 ]

View file

@ -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

View file

@ -0,0 +1,90 @@
<?php
namespace App\Controller;
use App\Controller\ControllerBase,
App\Model\Data\Callback as CallbackModel,
App\Model\Data\Request as RequestModel,
App\Model\Data\RequestMeta as RequestMetaModel,
App\Model\Data\UserActivation;
class ApiController extends ControllerBase
{
/**
* This is the action that the API to be
* tested should make it's callback to. So we can catch it.
*
* @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());
}
}
}
/**
* 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');
}
}
}

View file

@ -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());
}
}
}
}

View file

@ -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();

View file

@ -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');
}
}
}
}
}

View file

@ -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()

View file

@ -0,0 +1,70 @@
<?php
namespace App\Listener;
use Phalcon\Events\Event,
Phalcon\Mvc\Dispatcher,
Phalcon\Mvc\User\Plugin,
Phalcon\Mvc\Dispatcher\Exception as DispatcherException;
use Httpcb\Acl;
class AccessListener extends Plugin
{
protected $_ignored_resources = [
'index',
'error'
];
/**
* @param Event $event
* @param Dispatcher $dispatcher
* @return bool
* @throws DispatcherException
*/
public function beforeExecuteRoute(Event $event, Dispatcher $dispatcher) : bool
{
// We only have two roles for now, authenticated users and guests.
if ($this->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;
}
}

View file

@ -1,53 +0,0 @@
<?php
namespace App\Listener;
use Phalcon\Events\Event,
Phalcon\Mvc\Dispatcher,
Phalcon\Mvc\User\Plugin;
use Httpcb\Acl;
class AclListener extends Plugin
{
public function beforeExecuteRoute(Event $event, Dispatcher $dispatcher) : bool
{
// We only have two roles for now, authenticated users and guests.
if ($this->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;
}
}