Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
892a7e7
Cake3 Authorization
bravo-kernel Feb 13, 2015
dffafcf
Fixes acl-identifier casing
bravo-kernel Feb 13, 2015
f0c2051
Removes debugging
bravo-kernel Feb 13, 2015
7cdd947
Fixes broken tests
bravo-kernel Feb 13, 2015
77ec5fa
Removes newline function brackets
bravo-kernel Feb 13, 2015
56d1a00
Removes inflection for actions
bravo-kernel Feb 13, 2015
c34fe5e
Underscores protected functions as requested #discussion_r24676385
bravo-kernel Feb 15, 2015
1fcffc1
Extends tests + fixes allowAdmin logic
bravo-kernel Feb 15, 2015
3e03b1a
Removes inflection forcing strict acl.ini definitions
bravo-kernel Feb 15, 2015
4f4da0c
Adds tests for _iniConstruct() and _iniDeconstruct()
bravo-kernel Feb 15, 2015
976be8b
Enables access to all actions (prefixed and non-prefixed) except for…
bravo-kernel Feb 15, 2015
f08b942
Improves test for 'allowAdmin'
bravo-kernel Feb 15, 2015
7eafc85
Adds case sensitity tests
bravo-kernel Feb 15, 2015
eaa03ef
Removes obsolete tests
bravo-kernel Feb 15, 2015
9f17568
Updates descriptions
bravo-kernel Feb 15, 2015
2f4f1b9
Updates descriptions
bravo-kernel Feb 15, 2015
acb48f2
Merge branch 'cake3-prefixing' of git://github.com/bravo-kernel/cakep…
bravo-kernel Feb 15, 2015
af2c74f
Sort class order
bravo-kernel Feb 15, 2015
2f9706a
Adds tests for long names using Dashed Routes
bravo-kernel Feb 16, 2015
35f2db9
Renames tests for long_underscored_actions
bravo-kernel Feb 16, 2015
26643b4
Adds base documentation
bravo-kernel Feb 16, 2015
d96f9f2
Fixes typo
bravo-kernel Feb 16, 2015
88fb568
Removes public example
bravo-kernel Feb 16, 2015
6a4bd8d
Changes superadmin example
bravo-kernel Feb 17, 2015
dd505e3
Correctes superadmin constant
bravo-kernel Feb 17, 2015
07c7848
Removes obsolete guest checks + extends wildcard tests
bravo-kernel Feb 17, 2015
d71951c
Tests cleanup and refactoring
bravo-kernel Feb 17, 2015
1a05d5d
PHPCS fixes
bravo-kernel Feb 17, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions docs/authorize.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# TinyAuth Autorization

Enable TinyAuth Authorize if you want to add instant (and easy) role based
access to your application.

## Enabling

Assuming you already have Authentiction set up correctly you can enable
Authorization in your controllers beforeFilter like this example:

```php
// src/Controller/AppController

use Cake\Event\Event;

public function beforeFilter(Event $event)
{
parent::beforeFilter($event);
$this->loadComponent('Auth', [
'authorize' => [
'TinyAuth.Tiny' => [
'autoClearCache' => true,
'allowUser' => false,
'allowAdmin' => false,
'adminRole' => 'admin',
'superAdminRole' => null
]
]
]);
}
```

## Roles

You need to define some roles for Authorize to work, for example:

```php
// config/app_custom.php


/**
* Optionally define constants for easy referencing throughout your code
*/
define('ROLE_USERS', 1);
define('ROLE_ADMINS', 2);
define('ROLE_SUPERADMIN', 9);

return [
'Roles' => [
'user' => ROLE_USERS,
'admin' => ROLE_ADMINS,
'superadmin' => ROLE_SUPERADMIN
]
];
```

## acl.ini

Authorize expects an ``acl.ini`` file in your config directory.
Use it to specify who gets access to which resources.

The section key syntax follows the CakePHP naming convention for plugins.

Make sure to create an entry for each action you want to expose and use:

- one or more role names (groups granted access)
- the ``*`` wildcard to allow access to all authenticated users

```ini
; ----------------------------------------------------------
; Userscontroller
; ----------------------------------------------------------
[Users]
index = user, admin, undefined-role
edit, view = user, admin
* = admin
; ----------------------------------------------------------
; UsersController using /api prefixed route
; ----------------------------------------------------------
[api/Users]
view = user
* = admin
; ----------------------------------------------------------
; UsersController using /admin prefixed route
; ----------------------------------------------------------
[admin/Users]
* = admin
; ----------------------------------------------------------
; AccountsController in plugin named Accounts
; ----------------------------------------------------------
[Accounts.Accounts]
view, edit = user
* = admin
; ----------------------------------------------------------
; AccountsController in plugin named Accounts using /admin
; prefixed route
; ----------------------------------------------------------
[Accounts.admin/Accounts]
* = admin
; ----------------------------------------------------------
; CompaniesController in plugin named Accounts
; ----------------------------------------------------------
[Accounts.Companies]
view, edit = user
* = admin
; ----------------------------------------------------------
; CompaniesController in plugin named Accounts using /admin
; prefixed route
; ----------------------------------------------------------
[Accounts.admin/Companies]
* = admin
```

## Configuration

Authorize supports the following configuration options.

Option | Type|Description
--------|------------
allowUser|boolean|True will give authenticated users access to all resources except those using the `adminPrefix`
allowAdmin|boolean|True will give users with a role id matching `adminRole` access to all resources using the `adminPrefix`
adminRole|int|Id of the role you will use as admins. Users with this role are granted access to all actions using `adminPrefix` but only when `allowAdmin` is enabled
superAdminRole|int|Id of the super admin role. Users with this role will have access to ALL resources.
adminPrefix|string|Name of the prefix used for admin pages. Defaults to admin.
autoClearCache|Boolean|True will generate a new acl cache file every time.
aclKey|string|Name of the column holding your user role id (only for single role per user/BT)
aclTable|string|Name of the table holding your user roles (only for multiple roles per user/HABTM)
157 changes: 98 additions & 59 deletions src/Auth/TinyAuthorize.php
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<?php
namespace TinyAuth\Auth;

use Cake\Core\Configure;
use Cake\Cache\Cache;
use Cake\Utility\Inflector;
use Cake\Utility\Hash;
use Cake\Auth\BaseAuthorize;
use Cake\Network\Request;
use Cake\Cache\Cache;
use Cake\Controller\ComponentRegistry;
use Cake\Core\Configure;
use Cake\Network\Request;
use Cake\ORM\TableRegistry;
use Cake\Utility\Hash;
use Cake\Utility\Inflector;

if (!defined('CLASS_USER')) {
define('CLASS_USER', 'Users'); // override if you have it in a plugin: PluginName.Users etc
Expand Down Expand Up @@ -41,18 +41,18 @@ class TinyAuthorize extends BaseAuthorize {

protected $_acl = null;

protected $_defaultConfig = array(
'superadminRole' => null, // quick way to allow access to every action
'allowUser' => false, // quick way to allow user access to non prefixed urls
'allowAdmin' => false, // quick way to allow admin access to admin prefixed urls
'adminPrefix' => 'admin_',
'adminRole' => null, // needed together with adminPrefix if allowAdmin is enabled
protected $_defaultConfig = [
'adminRole' => null, // id of the admin role used by allowAdmin
'superAdminRole' => null, // id of super admin role granted access to ALL resources
'allowUser' => false, // enable to allow ALL roles access to all actions except prefixed with 'adminPrefix'
'allowAdmin' => false, // enable to allow admin role access to all 'adminPrefix' prefixed urls
'adminPrefix' => 'admin', // admin prefix used by allowAdmin
'cache' => AUTH_CACHE,
'cacheKey' => 'tiny_auth_acl',
'autoClearCache' => false, // usually done by Cache automatically in debug mode,
'aclTable' => 'Roles', // only for multiple roles per user (HABTM)
'aclKey' => 'role_id', // only for single roles per user (BT)
);
];

/**
* TinyAuthorize::__construct()
Expand Down Expand Up @@ -97,8 +97,7 @@ public function authorize($user, Request $request) {
trigger_error(sprintf('Missing acl information (%s) in user session', $acl));
$roles = array();
}

return $this->validate($roles, $request->params['plugin'], $request->params['controller'], $request->params['action']);
return $this->validate($roles, $request);
}

/**
Expand All @@ -111,63 +110,61 @@ public function authorize($user, Request $request) {
* @param string $action
* @return bool Success
*/
public function validate($roles, $plugin, $controller, $action) {
$action = Inflector::underscore($action);
$controller = Inflector::underscore($controller);
$plugin = Inflector::underscore($plugin);
//public function validate($roles, $plugin, $controller, $action) {
public function validate($roles, Request $request) {
// construct the iniKey and iniMap for easy lookups
$iniKey = $this->_constructIniKey($request);
$availableRoles = Configure::read($this->_config['aclTable']);

// Give any logged in user access to ALL actions when `allowUser` is
// enabled except when the `adminPrefix` is being used.
if (!empty($this->_config['allowUser'])) {
// all user actions are accessable for logged in users
if (mb_strpos($action, $this->_config['adminPrefix']) !== 0) {
if (empty($request->params['prefix'])) {
return true;
}
if ($request->params['prefix'] != $this->_config['adminPrefix']) {
return true;
}
}

// allow access to all /admin prefixed actions for users belonging to
// the specified adminRole id.
if (!empty($this->_config['allowAdmin']) && !empty($this->_config['adminRole'])) {
// all admin actions are accessable for logged in admins
if (mb_strpos($action, $this->_config['adminPrefix']) === 0) {
if (in_array((string)$this->_config['adminRole'], $roles)) {
if (!empty($request->params['prefix']) && $request->params['prefix'] === $this->_config['adminPrefix']) {
if (in_array($this->_config['adminRole'], $roles)) {
return true;
}
}
}

if ($this->_acl === null) {
$this->_acl = $this->_getAcl();
}

// allow_all check
if (!empty($this->_config['superadminRole'])) {
// allow logged in super admins access to all resources
if (!empty($this->_config['superAdminRole'])) {
foreach ($roles as $role) {
if ($role == $this->_config['superadminRole']) {
if ($role === $this->_config['superAdminRole']) {
return true;
}
}
}

// controller wildcard
if (isset($this->_acl[$controller]['*'])) {
$matchArray = $this->_acl[$controller]['*'];
if (in_array('-1', $matchArray)) {
return true;
}
// generate ACL if not already set
if ($this->_acl === null) {
$this->_acl = $this->_getAcl();
}

// allow access if user has a role with wildcard access to the resource
if (isset($this->_acl[$iniKey]['actions']['*'])) {
$matchArray = $this->_acl[$iniKey]['actions']['*'];
foreach ($roles as $role) {
if (in_array((string)$role, $matchArray)) {
return true;
}
}
}

// specific controller/action
if (!empty($controller) && !empty($action)) {
if (array_key_exists($controller, $this->_acl) && !empty($this->_acl[$controller][$action])) {
$matchArray = $this->_acl[$controller][$action];

// direct access? (even if he has no roles = GUEST)
if (in_array('-1', $matchArray)) {
return true;
}

// normal access (rolebased)
// allow access if user has been granted access to the specific resource
if (isset($this->_acl[$iniKey]['actions'])) {
if(array_key_exists($request->action, $this->_acl[$iniKey]['actions']) && !empty($this->_acl[$iniKey]['actions'][$request->action])) {
$matchArray = $this->_acl[$iniKey]['actions'][$request->action];
foreach ($roles as $role) {
if (in_array((string)$role, $matchArray)) {
return true;
Expand Down Expand Up @@ -200,13 +197,13 @@ protected function _getAcl($path = null) {
$path = ROOT . DS . 'config' . DS;
}

$res = array();
if ($this->_config['autoClearCache'] && Configure::read('debug') > 0) {
Cache::delete($this->_config['cacheKey'], $this->_config['cache']);
}
if (($roles = Cache::read($this->_config['cacheKey'], $this->_config['cache'])) !== false) {
return $roles;
}

if (!file_exists($path . ACL_FILE)) {
touch($path . ACL_FILE);
}
Expand Down Expand Up @@ -234,39 +231,42 @@ protected function _getAcl($path = null) {
return array();
}

$res = [];
foreach ($iniArray as $key => $array) {
list($plugin, $controllerName) = pluginSplit($key);
$controllerName = Inflector::underscore($controllerName);
$res[$key] = $this->_deconstructIniKey($key);

foreach ($array as $actions => $roles) {
$actions = explode(',', $actions);
// get all roles used in the current ini section
$roles = explode(',', $roles);
$actions = explode(',', $actions);

foreach ($roles as $key => $role) {
foreach ($roles as $roleId => $role) {
if (!($role = trim($role))) {
continue;
}
// prevent undefined roles appearing in the iniMap
if (!array_key_exists($role, $availableRoles) && $role != '*') {
unset($roles[$roleId]);
continue;
}
if ($role === '*') {
unset($roles[$key]);
unset($roles[$roleId]);
$roles = array_merge($roles, array_keys(Configure::read($this->_config['aclTable'])));
}
}

// process actions
foreach ($actions as $action) {
if (!($action = trim($action))) {
continue;
}
$actionName = Inflector::underscore($action);

foreach ($roles as $role) {
if (!($role = trim($role)) || $role === '*') {
continue;
}
$newRole = Configure::read($this->_config['aclTable'] . '.' . strtolower($role));
if (!empty($res[$controllerName][$actionName]) && in_array((string)$newRole, $res[$controllerName][$actionName])) {
continue;
}
$res[$controllerName][$actionName][] = $newRole;
$res[$key]['actions'][$action][] = $newRole;

}
}
}
Expand All @@ -275,4 +275,43 @@ protected function _getAcl($path = null) {
return $res;
}

/**
* Deconstructs an ACL ini section key into a named array with ACL parts
*
* @param string INI section key as found in acl.ini
* @return array Hash with named keys for controller, plugin and prefix
*/
protected function _deconstructIniKey($key) {
$res = [
'plugin' => null,
'prefix' => null
];

if (strpos($key, '.') !== false) {
list($res['plugin'], $key) = explode('.', $key);
}
if (strpos($key, '/') !== false) {
list($res['prefix'], $key) = explode('/', $key);
}
$res['controller'] = $key;
return $res;
}

/**
* Constructs an ACL ini section key from a given CakeRequest
*
* @param Cake\Network\Request $request The request needing authorization.
* @return array Hash with named keys for controller, plugin and prefix
*/
protected function _constructIniKey(Request $request) {
$res = $request->params['controller'];
if (!empty($request->params['prefix'])) {
$res = $request->params['prefix'] . "/$res";
}
if (!empty($request->params['plugin'])) {
$res = $request->params['plugin'] . ".$res";
}
return $res;
}

}
Loading