diff --git a/docs/authorize.md b/docs/authorize.md new file mode 100644 index 00000000..427ccdd4 --- /dev/null +++ b/docs/authorize.md @@ -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) diff --git a/src/Auth/TinyAuthorize.php b/src/Auth/TinyAuthorize.php index 8a165cc9..b6233974 100644 --- a/src/Auth/TinyAuthorize.php +++ b/src/Auth/TinyAuthorize.php @@ -1,14 +1,14 @@ 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() @@ -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); } /** @@ -111,45 +110,50 @@ 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; @@ -157,17 +161,10 @@ public function validate($roles, $plugin, $controller, $action) { } } - // 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; @@ -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); } @@ -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; + } } } @@ -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; + } + } diff --git a/tests/TestCase/Auth/TinyAuthorizeTest.php b/tests/TestCase/Auth/TinyAuthorizeTest.php index 462c3469..533f7b4f 100644 --- a/tests/TestCase/Auth/TinyAuthorizeTest.php +++ b/tests/TestCase/Auth/TinyAuthorizeTest.php @@ -1,13 +1,13 @@ request = new Request(); $aclData = <<assertTrue(file_exists(TMP . 'acl.ini')); - Configure::write('Roles', array('user' => 1, 'moderator' => 2, 'admin' => 3, 'public' => -1)); + Configure::write('Roles', [ + 'user' => 1, + 'moderator' => 2, + 'admin' => 3, + 'public' => -1 + ]); } public function tearDown() { @@ -68,11 +132,11 @@ public function tearDown() { * @return void */ public function testConstructor() { - $object = new TestTinyAuthorize($this->Collection, array( + $object = new TestTinyAuthorize($this->Collection, [ 'aclTable' => 'AuthRole', 'aclKey' => 'auth_role_id', 'autoClearCache' => true, - )); + ]); $this->assertEquals('AuthRole', $object->config('aclTable')); $this->assertEquals('auth_role_id', $object->config('aclKey')); } @@ -81,26 +145,145 @@ public function testConstructor() { * @return void */ public function testGetAcl() { - $object = new TestTinyAuthorize($this->Collection, array('autoClearCache' => true)); + $object = new TestTinyAuthorize($this->Collection, ['autoClearCache' => true]); $res = $object->getAcl(); - $expected = array( - 'users' => array( - 'edit' => array(1), - 'admin_index' => array(3) - ), - 'comments' => array( - 'add' => array(1), - 'edit' => array(1), - 'delete' => array(1), - '*' => array(3), - ), - 'tags' => array( - 'add' => array(1, 2, 3, -1), - 'very_long_action_name_action' => array(1), - 'public_action' => array(-1) - ), - ); + $expected = [ + 'Tags' => [ + 'controller' => 'Tags', + 'prefix' => null, + 'plugin' => null, + 'actions' => [ + 'index' => [1], + 'edit' => [1], + 'delete' => [3], + 'public_action' => [-1], + 'very_long_underscored_action' => [1], + 'veryLongActionNameAction' => [1] + ] + ], + 'admin/Tags' => [ + 'controller' => 'Tags', + 'prefix' => 'admin', + 'plugin' => null, + 'actions' => [ + 'index' => [1], + 'edit' => [1], + 'delete' => [3], + 'public_action' => [-1], + 'very_long_underscored_action' => [1], + 'veryLongActionNameAction' => [1] + ] + ], + 'Tags.Tags' => [ + 'controller' => 'Tags', + 'prefix' => null, + 'plugin' => 'Tags', + 'actions' => [ + 'index' => [1], + 'edit' => [1], + 'view' => [1], + 'delete' => [3], + 'public_action' => [-1], + 'very_long_underscored_action' => [1], + 'veryLongActionNameAction' => [1] + ] + ], + 'Tags.admin/Tags' => [ + 'controller' => 'Tags', + 'prefix' => 'admin', + 'plugin' => 'Tags', + 'actions' => [ + 'index' => [1], + 'edit' => [1], + 'view' => [1], + 'delete' => [3], + 'public_action' => [-1], + 'very_long_underscored_action' => [1], + 'veryLongActionNameAction' => [1] + ] + ], + 'special/Comments' => [ + 'controller' => 'Comments', + 'prefix' => 'special', + 'plugin' => null, + 'actions' => [ + '*' => [3] + ] + ], + 'Comments.special/Comments' => [ + 'controller' => 'Comments', + 'prefix' => 'special', + 'plugin' => 'Comments', + 'actions' => [ + '*' => [3] + ] + ], + 'Posts' => [ + 'controller' => 'Posts', + 'prefix' => null, + 'plugin' => null, + 'actions' => [ + '*' => [1, 2, 3, -1] + ] + ], + 'admin/Posts' => [ + 'controller' => 'Posts', + 'prefix' => 'admin', + 'plugin' => null, + 'actions' => [ + '*' => [1, 2, 3, -1] + ] + ], + 'Posts.Posts' => [ + 'controller' => 'Posts', + 'prefix' => null, + 'plugin' => 'Posts', + 'actions' => [ + '*' => [1, 2, 3, -1] + ] + ], + 'Posts.admin/Posts' => [ + 'controller' => 'Posts', + 'prefix' => 'admin', + 'plugin' => 'Posts', + 'actions' => [ + '*' => [1, 2, 3, -1] + ] + ], + 'Blogs' => [ + 'controller' => 'Blogs', + 'prefix' => null, + 'plugin' => null, + 'actions' => [ + '*' => [2] + ] + ], + 'admin/Blogs' => [ + 'controller' => 'Blogs', + 'prefix' => 'admin', + 'plugin' => null, + 'actions' => [ + '*' => [2] + ] + ], + 'Blogs.Blogs' => [ + 'controller' => 'Blogs', + 'prefix' => null, + 'plugin' => 'Blogs', + 'actions' => [ + '*' => [2] + ] + ], + 'Blogs.admin/Blogs' => [ + 'controller' => 'Blogs', + 'prefix' => 'admin', + 'plugin' => 'Blogs', + 'actions' => [ + '*' => [2] + ] + ] + ]; //debug($res); $this->assertEquals($expected, $res); } @@ -109,22 +292,64 @@ public function testGetAcl() { * @return void */ public function testBasicUserMethodDisallowed() { - $this->request->params['controller'] = 'users'; - $this->request->params['action'] = 'edit'; - - $object = new TestTinyAuthorize($this->Collection, array('autoClearCache' => true)); + $object = new TestTinyAuthorize($this->Collection, [ + 'autoClearCache' => true + ]); $this->assertEquals('Roles', $object->config('aclTable')); $this->assertEquals('role_id', $object->config('aclKey')); - $user = array( - 'role_id' => 4, - ); + // All tests performed against this action + $this->request->params['action'] = 'add'; + + // Test standard controller + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = null; + $this->request->params['plugin'] = 'Tags'; + + $user = ['role_id' => 4]; // invalid non-existing role $res = $object->authorize($user, $this->request); $this->assertFalse($res); - $user = array( - 'role_id' => 3, - ); + $user = ['role_id' => 1]; // valid role without authorization + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + // Test standard controller with /admin prefix + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = null; + $this->request->params['plugin'] = null; + + $user = ['role_id' => 4]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + $user = ['role_id' => 1]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + // Test plugin controller + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = null; + $this->request->params['plugin'] = 'Tags'; + + $user = ['role_id' => 4]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + $user = ['role_id' => 1]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + // Test plugin controller with /admin prefix + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = 'admin'; + $this->request->params['plugin'] = 'Tags'; + + $user = ['role_id' => 4]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + $user = ['role_id' => 1]; $res = $object->authorize($user, $this->request); $this->assertFalse($res); } @@ -133,46 +358,199 @@ public function testBasicUserMethodDisallowed() { * @return void */ public function testBasicUserMethodAllowed() { - $this->request->params['controller'] = 'users'; + $object = new TestTinyAuthorize($this->Collection, [ + 'autoClearCache' => true + ]); + + // Test standard controller + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = null; + $this->request->params['plugin'] = null; + + $user = [ 'role_id' => 1 ]; $this->request->params['action'] = 'edit'; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); - $object = new TestTinyAuthorize($this->Collection, array('autoClearCache' => true)); + $this->request->params['action'] = 'delete'; + $user = ['role_id' => 3]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); - // single role_id field in users table - $user = array( - 'role_id' => 1, - ); + // Test standard controller with /admin prefix + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = 'admin'; + $this->request->params['plugin'] = null; + + $user = [ 'role_id' => 1 ]; + $this->request->params['action'] = 'edit'; $res = $object->authorize($user, $this->request); $this->assertTrue($res); - $this->request->params['action'] = 'admin_index'; + $user = ['role_id' => 3]; + $this->request->params['action'] = 'delete'; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); - $user = array( - 'role_id' => 3, - ); + // Test plugin controller + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = null; + $this->request->params['plugin'] = 'Tags'; + + $user = ['role_id' => 1]; + $this->request->params['action'] = 'edit'; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + $user = ['role_id' => 3]; + $this->request->params['action'] = 'delete'; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + // Test plugin controller with /admin prefix + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = 'admin'; + $this->request->params['plugin'] = 'Tags'; + + $user = ['role_id' => 1]; + $this->request->params['action'] = 'edit'; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + $user = ['role_id' => 3]; + $this->request->params['action'] = 'delete'; $res = $object->authorize($user, $this->request); $this->assertTrue($res); } /** + * Tests using incorrect casing, enforces strict acl.ini definitions. + * * @return void */ - public function testBasicUserMethodAllowedWithLongActionNames() { + public function testCaseSensitivity() { + $object = new TestTinyAuthorize($this->Collection, [ + 'autoClearCache' => true] + ); + + // All tests performed against this action + $this->request->params['action'] = 'index'; + + // Test incorrect controller casing $this->request->params['controller'] = 'tags'; - $this->request->params['action'] = 'very_long_action_name_action'; + $this->request->params['prefix'] = null; + $this->request->params['plugin'] = null; - $object = new TestTinyAuthorize($this->Collection, array('autoClearCache' => true)); + $user = [ 'role_id' => 1 ]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); - // single role_id field in users table - $user = array( - 'role_id' => 1, - ); + // Test incorrect controller casing with /admin prefix + $this->request->params['controller'] = 'tags'; + $this->request->params['prefix'] = 'admin'; + $this->request->params['plugin'] = null; + + $user = [ 'role_id' => 1 ]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + // Test correct controller casing with incorrect prefix casing + $this->request->params['controller'] = 'Users'; + $this->request->params['prefix'] = 'Admin'; + $this->request->params['plugin'] = null; + + $user = [ 'role_id' => 1 ]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + // Test incorrect plugin controller casing + $this->request->params['controller'] = 'tags'; + $this->request->params['prefix'] = null; + $this->request->params['plugin'] = 'Tags'; + + $user = [ 'role_id' => 1 ]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + // Test correct plugin controller with incorrect plugin casing + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = null; + $this->request->params['plugin'] = 'tags'; + + $user = [ 'role_id' => 1 ]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + // Test correct plugin controller with correct plugin but incorrect prefix casing + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = 'Admin'; + $this->request->params['plugin'] = 'Tags'; + + $user = [ 'role_id' => 1 ]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + } + + /** + * @return void + */ + public function testBasicUserMethodAllowedWithLongActionNames() { + $object = new TestTinyAuthorize($this->Collection, [ + 'autoClearCache' => true + ]); + + // All tests performed against this action + $this->request->params['action'] = 'veryLongActionNameAction'; + + // Test standard controller + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = null; + $this->request->params['plugin'] = null; + + $user = ['role_id' => 1]; $res = $object->authorize($user, $this->request); $this->assertTrue($res); - $user = array( - 'role_id' => 3, - ); + $user = ['role_id' => 2]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + // Test standard controller with /admin prefix + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = 'admin'; + $this->request->params['plugin'] = null; + + $user = [ 'role_id' => 1 ]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + $user = ['role_id' => 2]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + // Test plugin controller + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = null; + $this->request->params['plugin'] = 'Tags'; + + $user = [ 'role_id' => 1 ]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + $user = ['role_id' => 2]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + // Test plugin controller with /admin prefix + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = 'admin'; + $this->request->params['plugin'] = 'Tags'; + + $user = [ 'role_id' => 1 ]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + $user = ['role_id' => 2]; $res = $object->authorize($user, $this->request); $this->assertFalse($res); } @@ -180,134 +558,406 @@ public function testBasicUserMethodAllowedWithLongActionNames() { /** * @return void */ - public function testBasicUserMethodAllowedMultiRole() { - $this->request->params['controller'] = 'Users'; - $this->request->params['action'] = 'admin_index'; + public function testBasicUserMethodAllowedWithLongActionNamesUnderscored() { + $object = new TestTinyAuthorize($this->Collection, [ + 'autoClearCache' => true + ]); - $object = new TestTinyAuthorize($this->Collection, array('autoClearCache' => true)); + // All tests performed against this action + $this->request->params['action'] = 'very_long_underscored_action'; - // flat list of roles - $user = array( - 'Roles' => array(1, 3), - ); + // Test standard controller + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = null; + $this->request->params['plugin'] = null; + + $user = ['role_id' => 1]; $res = $object->authorize($user, $this->request); $this->assertTrue($res); - // verbose role defition using the new 2.x contain param for Auth - $user = array( - 'Roles' => array(array('id' => 1, 'RoleUsers' => array()), array('id' => 3, 'RoleUsers' => array())), - ); + $user = ['role_id' => 2]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + // Test standard controller with /admin prefix + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = 'admin'; + $this->request->params['plugin'] = null; + + $user = [ 'role_id' => 1 ]; $res = $object->authorize($user, $this->request); $this->assertTrue($res); + + $user = ['role_id' => 2]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + // Test plugin controller + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = null; + $this->request->params['plugin'] = 'Tags'; + + $user = [ 'role_id' => 1 ]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + $user = ['role_id' => 2]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + // Test plugin controller with /admin prefix + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = 'admin'; + $this->request->params['plugin'] = 'Tags'; + + $user = [ 'role_id' => 1 ]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + $user = ['role_id' => 2]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); } /** * @return void */ - public function testBasicUserMethodAllowedWildcard() { + public function testBasicUserMethodAllowedMultiRole() { + $object = new TestTinyAuthorize($this->Collection, [ + 'autoClearCache' => true + ]); + $this->request->params['controller'] = 'Tags'; - $this->request->params['action'] = 'public_action'; + $this->request->params['action'] = 'delete'; - $object = new TestTinyAuthorize($this->Collection, array('autoClearCache' => true)); + // Flat list of roles + $user = [ + 'Roles' => [1, 3] + ]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); - $user = array( - 'role_id' => 6, - ); + // Verbose role defition using the new 2.x contain param for Auth + $user = [ + 'Roles' => [ + ['id' => 1, 'RoleUsers' => []], + ['id' => 3, 'RoleUsers' => []] + ], + ]; + + $user = [ + 'Roles' => [ + ['id' => 1, 'RoleUsers' => []], + ['id' => 3, 'RoleUsers' => []] + ] + ]; $res = $object->authorize($user, $this->request); $this->assertTrue($res); } /** + * Tests access to a controller that uses the * wildcard for both the + * action and the allowed groups (* = *). + * + * Note: users without a valid/defined role will not be granted access. + * * @return void */ - public function testUserMethodsAllowed() { - $this->request->params['controller'] = 'Users'; - $this->request->params['action'] = 'some_action'; + public function testBasicUserMethodAllowedWildcard() { + $object = new TestTinyAuthorize($this->Collection, [ + 'autoClearCache' => true + ]); - $object = new TestTinyAuthorize($this->Collection, array('allowUser' => true, 'autoClearCache' => true)); + // All tests performed against this action + $this->request->params['action'] = 'any_action'; - $user = array( - 'role_id' => 1, - ); + // Test standard controller + $this->request->params['controller'] = 'Posts'; + $this->request->params['prefix'] = null; + $this->request->params['plugin'] = null; + + $user = ['role_id' => 2]; $res = $object->authorize($user, $this->request); $this->assertTrue($res); - $this->request->params['controller'] = 'Users'; - $this->request->params['action'] = 'admin_index'; + $user = ['role_id' => 123]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); - $object = new TestTinyAuthorize($this->Collection, array('allowUser' => true, 'autoClearCache' => true)); + // Test *=* for standard controller with /admin prefix + $this->request->params['controller'] = 'Posts'; + $this->request->params['prefix'] = 'admin'; + $this->request->params['plugin'] = null; - $user = array( - 'role_id' => 1, - ); + $user = ['role_id' => 2]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + $user = ['role_id' => 123]; $res = $object->authorize($user, $this->request); $this->assertFalse($res); - $user = array( - 'role_id' => 3, - ); + // Test *=* for plugin controller + $this->request->params['controller'] = 'Posts'; + $this->request->params['prefix'] = null; + $this->request->params['plugin'] = 'Posts'; + + $user = ['role_id' => 2]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + $user = ['role_id' => 123]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + // Test *=* for plugin controller with /admin prefix + $this->request->params['controller'] = 'Posts'; + $this->request->params['prefix'] = 'admin'; + $this->request->params['plugin'] = 'Posts'; + + $user = ['role_id' => 2]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + $user = ['role_id' => 123]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + } + + /** + * Tests access to a controller that uses the * wildcard for the action + * but combines it with a specific group (here: * = moderators). + * + * @return void + */ + public function testBasicUserMethodAllowedWildcardSpecificGroup() { + $object = new TestTinyAuthorize($this->Collection, [ + 'autoClearCache' => true + ]); + + // All tests performed against this action + $this->request->params['action'] = 'any_action'; + + // Test standard controller + $this->request->params['controller'] = 'Blogs'; + $this->request->params['prefix'] = null; + $this->request->params['plugin'] = null; + + $user = ['role_id' => 2]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + $user = ['role_id' => 3]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + // Test standard controller with /admin prefix + $this->request->params['controller'] = 'Blogs'; + $this->request->params['prefix'] = 'admin'; + $this->request->params['plugin'] = null; + + $user = ['role_id' => 2]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + $user = ['role_id' => 3]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + // Test plugin controlller + $this->request->params['controller'] = 'Blogs'; + $this->request->params['prefix'] = null; + $this->request->params['plugin'] = 'Blogs'; + + $user = ['role_id' => 2]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + $user = ['role_id' => 3]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + // Test plugin controlller with /admin prefix + $this->request->params['controller'] = 'Blogs'; + $this->request->params['prefix'] = 'admin'; + $this->request->params['plugin'] = 'Blogs'; + + $user = ['role_id' => 2]; $res = $object->authorize($user, $this->request); $this->assertTrue($res); + + $user = ['role_id' => 3]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); } + /** + * Tests with configuration setting 'allowUser' set to true, giving user + * access to all controller/actions except when prefixed with /admin + * * @return void */ - public function testAdminMethodsAllowed() { - $this->request->params['controller'] = 'Users'; - $this->request->params['action'] = 'some_action'; - $config = array('allowAdmin' => true, 'adminRole' => 3, 'autoClearCache' => true); + public function testUserMethodsAllowed() { + $object = new TestTinyAuthorize($this->Collection, [ + 'allowUser' => true, + 'autoClearCache' => true, + 'adminPrefix' => 'admin' + ]); - $object = new TestTinyAuthorize($this->Collection, $config); + // All tests performed against this action + $this->request->params['action'] = 'any_action'; - $user = array( - 'role_id' => 1, - ); + // Test standard controller + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = null; + $this->request->params['plugin'] = null; + + $user = ['role_id' => 1]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + $user = ['role_id' => 2]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + $user = ['role_id' => 3]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + // Test standard controller with /admin prefix. Note: users should NOT + // be allowed access here since the prefix matches the 'adminPrefix' + // configuration setting. + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = 'admin'; + $this->request->params['plugin'] = null; + + $user = ['role_id' => 1]; $res = $object->authorize($user, $this->request); $this->assertFalse($res); - $this->request->params['controller'] = 'users'; - $this->request->params['action'] = 'admin_index'; + $user = ['role_id' => 2]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); - $object = new TestTinyAuthorize($this->Collection, $config); + $user = ['role_id' => 3]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); - $user = array( - 'role_id' => 1, - ); + // Test plugin controller + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = null; + $this->request->params['plugin'] = 'Tags'; + + $user = ['role_id' => 1]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + $user = ['role_id' => 2]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + $user = ['role_id' => 3]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + // Test plugin controller with /admin prefix. Again: access should + // NOT be allowed because of matching 'adminPrefix' + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = 'admin'; + $this->request->params['plugin'] = 'Tags'; + + $user = ['role_id' => 1]; $res = $object->authorize($user, $this->request); $this->assertFalse($res); - $user = array( - 'role_id' => 3, - ); + $user = ['role_id' => 2]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + $user = ['role_id' => 3]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + // Test access to a standard controller using a prefix not matching the + // 'adminPrefix' => users should be allowed access. + $this->request->params['controller'] = 'Comments'; + $this->request->params['prefix'] = 'special'; + $this->request->params['plugin'] = null; + + $user = ['role_id' => 1]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + $user = ['role_id' => 2]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + $user = ['role_id' => 3]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + // Test access to a plugin controller using a prefix not matching the + // 'adminPrefix' => users should be allowed access. + $this->request->params['controller'] = 'Comments'; + $this->request->params['prefix'] = 'special'; + $this->request->params['plugin'] = 'Comments'; + + $user = ['role_id' => 1]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + $user = ['role_id' => 2]; + $res = $object->authorize($user, $this->request); + $this->assertTrue($res); + + $user = ['role_id' => 3]; $res = $object->authorize($user, $this->request); $this->assertTrue($res); } /** - * Should only be used in combination with Auth->allow() to mark those as public in the acl.ini, as well. - * Not necessary and certainly not recommended as acl.ini only. + * Test with enabled configuration settings 'allowAdmin' and 'adminRole' + * giving users having the adminRole ID access to all actions that are + * prefixed using the 'adminPrefix' configuration setting. * * @return void */ - public function testBasicUserMethodAllowedPublically() { - $this->request->params['controller'] = 'tags'; - $this->request->params['action'] = 'add'; + public function testAdminMethodsAllowed() { + $config = [ + 'allowAdmin' => true, + 'adminRole' => 3, + 'adminPrefix' => 'admin', + 'autoClearCache' => true + ]; + $object = new TestTinyAuthorize($this->Collection, $config); - $object = new TestTinyAuthorize($this->Collection, array('autoClearCache' => true)); + // All tests performed against this action + $this->request->params['action'] = 'any_action'; - $user = array( - 'role_id' => 2, - ); + // Test standard controller with /admin prefix + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = 'admin'; + $this->request->params['plugin'] = null; + + $user = ['role_id' => 1]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + $user = ['role_id' => 3]; $res = $object->authorize($user, $this->request); $this->assertTrue($res); - $this->request->params['controller'] = 'comments'; - $this->request->params['action'] = 'foo'; + // Test plugin controller with /admin prefix + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = 'admin'; + $this->request->params['plugin'] = 'Tags'; - $user = array( - 'role_id' => 3, - ); + $user = ['role_id' => 1]; + $res = $object->authorize($user, $this->request); + $this->assertFalse($res); + + $user = ['role_id' => 3]; $res = $object->authorize($user, $this->request); $this->assertTrue($res); } @@ -323,29 +973,31 @@ public function testWithRoleTable() { // We want the session to be used. Configure::delete('Roles'); + $object = new TestTinyAuthorize($this->Collection, [ + 'autoClearCache' => true + ]); - $this->request->params['controller'] = 'Users'; + // test standard controller + $this->request->params['controller'] = 'Tags'; $this->request->params['action'] = 'edit'; - $object = new TestTinyAuthorize($this->Collection, array('autoClearCache' => true)); - // User role is 4 here, though. Also contains left joined Role date here just to check that it works, too. - $user = array( - 'Roles' => array( + $user = [ + 'Roles' => [ 'id' => '4', - 'alias' => 'user', - ), + 'alias' => 'user' + ], 'role_id' => 4, - ); + ]; $res = $object->authorize($user, $this->request); $this->assertTrue($res); Configure::delete('Roles'); - $object = new TestTinyAuthorize($this->Collection, array('autoClearCache' => true)); + $object = new TestTinyAuthorize($this->Collection, ['autoClearCache' => true]); - $user = array( - 'role_id' => 6, - ); + $user = [ + 'role_id' => 6 + ]; $res = $object->authorize($user, $this->request); $this->assertFalse($res); @@ -353,59 +1005,209 @@ public function testWithRoleTable() { // Multi-role test - failure Configure::delete('Roles'); - $object = new TestTinyAuthorize($this->Collection, array('autoClearCache' => true)); - - $user = array( - 'Roles' => array( - array('id' => 7, 'alias' => 'user'), - array('id' => 8, 'alias' => 'partner'), - ) - ); + $object = new TestTinyAuthorize($this->Collection, ['autoClearCache' => true]); + + $user = [ + 'Roles' => [ + ['id' => 7, 'alias' => 'user'], + ['id' => 8, 'alias' => 'partner'] + ] + ]; $res = $object->authorize($user, $this->request); $this->assertFalse($res); $this->assertTrue((bool)(Configure::read('Roles'))); Configure::delete('Roles'); - $object = new TestTinyAuthorize($this->Collection, array('autoClearCache' => true)); + $object = new TestTinyAuthorize($this->Collection, ['autoClearCache' => true]); // Multi-role test - $user = array( - 'Roles' => array( - array('id' => 4, 'alias' => 'user'), - array('id' => 6, 'alias' => 'partner'), - ) - ); + $user = [ + 'Roles' => [ + ['id' => 4, 'alias' => 'user'], + ['id' => 6, 'alias' => 'partner'], + ] + ]; $res = $object->authorize($user, $this->request); $this->assertTrue($res); } /** - * Tests superadmin role, allowed to all actions + * Tests superAdmin role, allowed to all actions * * @return void */ - public function testSuperadminRole() { - $object = new TestTinyAuthorize($this->Collection, array( + public function testSuperAdminRole() { + $object = new TestTinyAuthorize($this->Collection, [ 'autoClearCache' => true, - 'superadminRole' => 9 - )); + 'superAdminRole' => 9 + ]); $res = $object->getAcl(); - $user = array( - 'role_id' => 9, - ); - - foreach ($object->getAcl() as $controller => $actions) { - foreach ($actions as $action => $allowed) { - $this->request->params['controller'] = $controller; + $user = [ + 'role_id' => 9 + ]; + + foreach ($object->getAcl() as $resource) { + foreach ($resource['actions'] as $action => $allowed) { + $this->request->params['controller'] = $resource['controller']; + $this->request->params['prefix'] = $resource['prefix']; $this->request->params['action'] = $action; - $res = $object->authorize($user, $this->request); $this->assertTrue($res); } } } + /** + * Tests constructing an ACL ini section key using CakeRequest parameters + * + * @return void + */ + public function testIniConstruct() { + // Make protected function accessible + $object = new TestTinyAuthorize($this->Collection); + $reflection = new \ReflectionClass(get_class($object)); + $method = $reflection->getMethod('_constructIniKey'); + $method->setAccessible(true); + + // Test standard controller + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = null; + $this->request->params['plugin'] = null; + + $expected = 'Tags'; + $res = $method->invokeArgs($object, [$this->request]); + $this->assertEquals($expected, $res); + + // Test standard controller with /admin prefix + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = 'admin'; + $this->request->params['plugin'] = null; + + $expected = 'admin/Tags'; + $res = $method->invokeArgs($object, [$this->request]); + $this->assertEquals($expected, $res); + + // Test plugin controller + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = null; + $this->request->params['plugin'] = 'Tags'; + + $expected = 'Tags.Tags'; + $res = $method->invokeArgs($object, [$this->request]); + $this->assertEquals($expected, $res); + + // Test plugin controller with /admin prefix + $this->request->params['controller'] = 'Tags'; + $this->request->params['prefix'] = 'admin'; + $this->request->params['plugin'] = 'Tags'; + + $expected = 'Tags.admin/Tags'; + $res = $method->invokeArgs($object, [$this->request]); + $this->assertEquals($expected, $res); + } + + /** + * Tests deconstructing an ACL ini section key + * + * @return void + */ + public function testIniDeconstruct() { + // Make protected function accessible + $object = new TestTinyAuthorize($this->Collection); + $reflection = new \ReflectionClass(get_class($object)); + $method = $reflection->getMethod('_deconstructIniKey'); + $method->setAccessible(true); + + // Test standard controller + $key = 'Tags'; + $expected = [ + 'controller' => 'Tags', + 'plugin' => null, + 'prefix' => null + ]; + $res = $method->invokeArgs($object, [$key]); + $this->assertEquals($expected, $res); + + $key = 'tags'; // test incorrect casing + $res = $method->invokeArgs($object, [$key]); + $this->assertNotEquals($expected, $res); + + // Test standard controller with /admin prefix + $key = 'admin/Tags'; + $expected = [ + 'controller' => 'Tags', + 'prefix' => 'admin', + 'plugin' => null + ]; + $res = $method->invokeArgs($object, [$key]); + $this->assertEquals($expected, $res); + + $key = 'admin/tags'; + $res = $method->invokeArgs($object, [$key]); + $this->assertNotEquals($expected, $res); + + $key = 'Admin/tags'; + $res = $method->invokeArgs($object, [$key]); + $this->assertNotEquals($expected, $res); + + $key = 'Admin/Tags'; + $res = $method->invokeArgs($object, [$key]); + $this->assertNotEquals($expected, $res); + + // Test plugin controller without prefix + $key = 'Tags.Tags'; + $expected = [ + 'controller' => 'Tags', + 'prefix' => null, + 'plugin' => 'Tags' + ]; + $res = $method->invokeArgs($object, [$key]); + $this->assertEquals($expected, $res); + + $key = 'tags/tags'; + $res = $method->invokeArgs($object, [$key]); + $this->assertNotEquals($expected, $res); + + $key = 'tags/Tags'; + $res = $method->invokeArgs($object, [$key]); + $this->assertNotEquals($expected, $res); + + $key = 'Tags/tags'; + $res = $method->invokeArgs($object, [$key]); + $this->assertNotEquals($expected, $res); + + // Test plugin controller with /admin prefix + $key = 'Tags.admin/Tags'; + $expected = [ + 'controller' => 'Tags', + 'prefix' => 'admin', + 'plugin' => 'Tags' + ]; + $res = $method->invokeArgs($object, [$key]); + $this->assertEquals($expected, $res); + + $key = 'tags.admin/tags'; + $res = $method->invokeArgs($object, [$key]); + $this->assertNotEquals($expected, $res); + + $key = 'tags.Admin/tags'; + $res = $method->invokeArgs($object, [$key]); + $this->assertNotEquals($expected, $res); + + $key = 'tags.admin/Tags'; + $res = $method->invokeArgs($object, [$key]); + $this->assertNotEquals($expected, $res); + + $key = 'Tags.Admin/Tags'; + $res = $method->invokeArgs($object, [$key]); + $this->assertNotEquals($expected, $res); + + $key = 'Tags.Admin/tags'; + $res = $method->invokeArgs($object, [$key]); + $this->assertNotEquals($expected, $res); + } + } class TestTinyAuthorize extends TinyAuthorize {