<?php
namespace Croogo\Acl\Auth;
use Acl\Auth\BaseAuthorize;
use Cake\Cache\Cache;
use Cake\Controller\ComponentRegistry;
use Cake\Core\Configure;
use Cake\Core\Exception\Exception;
use Cake\Http\ServerRequest;
use Cake\Log\Log;
use Cake\Network\Request;
use Cake\ORM\TableRegistry;
use Cake\Utility\Inflector;
/**
* An authentication adapter for AuthComponent. Provides similar functionality
* to ActionsAuthorize class from CakePHP core _with_ caching capability.
*
* @package Croogo.Acl.Controller.Component.Auth
* @since 1.5
* @author Rachman Chavik <rchavik@xintesa.com>
* @see RowLevelAclComponent
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
* @link http://www.croogo.org
*/
class AclCachedAuthorize extends BaseAuthorize
{
protected $_defaultConfig = [
'actionMap' => [
'toggle' => 'update',
'moveup' => 'update',
'movedown' => 'update',
'process' => 'delete',
'index' => 'read',
'add' => 'create',
'edit' => 'update',
'view' => 'read',
'remove' => 'delete',
'create' => 'create',
'read' => 'read',
'update' => 'update',
'delete' => 'delete',
],
];
/**
* Constructor
*/
public function __construct(ComponentRegistry $registry, $config = [])
{
parent::__construct($registry, $config);
}
/**
* Checks whether $user is an administrator
*
* @param bool True if user has administrative role
*/
protected function _isAdmin($user)
{
static $Role = null;
if (empty($user['role_id'])) {
return false;
}
if (empty($this->_adminRole)) {
if (empty($Role)) {
$Role = TableRegistry::get('Croogo/Users.Roles');
$Role->addBehavior('Croogo/Core.Aliasable');
}
$this->_adminRole = $Role->byAlias('superadmin');
}
return $user['role_id'] == $this->_adminRole;
}
/**
* Get the action path for a given request.
*
* @see BaseAuthorize::action()
*/
public function action(ServerRequest $request, $path = '/:plugin/:prefix/:controller/:action')
{
$apiPath = Configure::read('Croogo.Api.path');
if (!$request->is('api')) {
return parent::action($request, $path);
}
$api = isset($request['api']) ? $apiPath : null;
if (isset($request['prefix'])) {
$prefix = $request['prefix'];
$action = str_replace($request['prefix'] . '_', '', $request['action']);
} else {
$prefix = null;
$action = $request['action'];
}
$plugin = empty($request['plugin']) ? null : str_replace('/', '\\', Inflector::camelize($request['plugin']));
$controller = Inflector::camelize($request['controller']);
$path = str_replace(
[$apiPath, ':prefix', ':plugin', ':controller', ':action'],
[$api, $prefix, $plugin, $controller, $action],
$this->getConfig('actionPath') . $path
);
$path = str_replace('//', '/', $path);
return trim($path, '/');
}
/**
* check request request authorization
*
*/
public function authorize($user, ServerRequest $request)
{
// Admin role is allowed to perform all actions, bypassing ACL
if ($this->_isAdmin($user)) {
return true;
}
$allowed = false;
$Acl = $this->_registry->load('Acl');
list($plugin, $userModel) = pluginSplit($this->getConfig('userModel'));
$action = $this->action($request);
$cacheName = 'permissions_' . (string)$user['id'];
if (($permissions = Cache::read($cacheName, 'permissions')) === false) {
$permissions = [];
Cache::write($cacheName, $permissions, 'permissions');
}
if (!isset($permissions[$action])) {
$userTable = TableRegistry::getTableLocator()->get($this->getConfig('userModel'));
$user = $userTable->get($user['id']);
$allowed = $Acl->check($user, $action);
$permissions[$action] = $allowed;
Cache::write($cacheName, $permissions, 'permissions');
$hit = false;
} else {
$allowed = $permissions[$action];
$hit = true;
}
if (Configure::read('debug')) {
$status = $allowed ? ' allowed.' : ' denied.';
$cached = $hit ? ' (cache hit)' : ' (cache miss)';
Log::write(LOG_ERR, $user['username'] . ' - ' . $action . $status . $cached);
}
if (!$allowed) {
return false;
}
if (!Configure::read('Access Control.rowLevel')) {
return $allowed;
}
// bail out when controller's primary model does not want row level acl
$controller = $this->_registry->getController();
$model = $controller->name;
$Model = $controller->{$model};
if ($Model && !$Model->behaviors()->has('RowLevelAcl')) {
return $allowed;
}
$primaryKey = $Model->primaryKey();
$ids = [];
if ($request->is('get') && $request->getParam('pass.0')) {
// collect id from actions such as: Nodes/admin_edit/1
$ids[] = $request->getParam('pass.0');
} elseif ($request->is('post') || $request->is('put')) {
$action = $request->getData('action');
if ($action) {
// collect ids from 'bulk' processing action
foreach ($request->getData($model) as $id => $flag) {
if (isset($flag[$primaryKey]) && $flag[$primaryKey] == 1) {
$ids[] = $id;
}
}
}
$id = $request->param('pass.0');
if ($id) {
$ids[] = $id;
}
}
foreach ($ids as $id) {
if (is_numeric($id)) {
try {
$allowed = $this->_authorizeByContent($user, $request, $id);
} catch (\Exception $e) {
$allowed = false;
}
} else {
continue;
}
if (!$allowed) {
break;
}
}
return $allowed;
}
/**
* Checks authorization by content
*
* @throws Exception
*/
protected function _authorizeByContent($user, ServerRequest $request, $id)
{
if (!isset($this->getConfig('actionMap')[$request->params['action']])) {
$message = __d(
'croogo',
'_authorizeByContent() - Access of un-mapped action "%1$s" in controller "%2$s"',
$request->action,
$request->controller
);
Log::critical($message);
throw new Exception($message);
}
list($plugin, $userModel) = pluginSplit($this->getConfig('userModel'));
$acoNode = [
'model' => $this->_registry->getController()->name,
'foreign_key' => $id,
];
$alias = sprintf('%s.%s', $acoNode['model'], $acoNode['foreign_key']);
$action = $this->getConfig('actionMap')[$request->param('action')];
$cacheName = 'permissions_content_' . strval($user['id']);
if (($permissions = Cache::read($cacheName, 'permissions')) === false) {
$permissions = [];
Cache::write($cacheName, $permissions, 'permissions');
}
if (!isset($permissions[$alias][$action])) {
$Acl = $this->_registry->load('Acl');
try {
$allowed = $Acl->check([$userModel => $user], $acoNode, $action);
} catch (\Exception $e) {
Log::warning('authorizeByContent: ' . $e->getMessage());
$allowed = false;
}
$permissions[$alias][$action] = $allowed;
Cache::write($cacheName, $permissions, 'permissions');
$hit = false;
} else {
$allowed = $permissions[$alias][$action];
$hit = true;
}
if (Configure::read('debug')) {
$status = $allowed ? ' allowed.' : ' denied.';
$cached = $hit ? ' (cache hit)' : ' (cache miss)';
Log::write(LOG_ERR, $user['username'] . ' - ' . $action . '/' . $id . $status . $cached);
}
return $allowed;
}
}