diff --git a/.gitignore b/.gitignore index 35028697..32bd2402 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /tmp/ /composer.lock /composer.phar +/.idea diff --git a/.travis.yml b/.travis.yml index 5c8770bd..0350e0b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ language: php sudo: false php: - - 5.4 - 5.5 - 5.6 - 7.0 diff --git a/README.md b/README.md index 7670368d..8674b714 100644 --- a/README.md +++ b/README.md @@ -3,31 +3,37 @@ [![Build Status](https://api.travis-ci.org/dereuromark/cakephp-tinyauth.svg?branch=master)](https://travis-ci.org/dereuromark/cakephp-tinyauth) [![Latest Stable Version](https://poser.pugx.org/dereuromark/cakephp-tinyauth/v/stable.svg)](https://packagist.org/packages/dereuromark/cakephp-tinyauth) [![Coverage Status](https://coveralls.io/repos/dereuromark/cakephp-tinyauth/badge.svg)](https://coveralls.io/r/dereuromark/cakephp-tinyauth) -[![Minimum PHP Version](http://img.shields.io/badge/php-%3E%3D%205.4-8892BF.svg)](https://php.net/) +[![Minimum PHP Version](http://img.shields.io/badge/php-%3E%3D%205.5-8892BF.svg)](https://php.net/) [![License](https://poser.pugx.org/dereuromark/cakephp-tinyauth/license.svg)](https://packagist.org/packages/dereuromark/cakephp-tinyauth) [![Total Downloads](https://poser.pugx.org/dereuromark/cakephp-tinyauth/d/total.svg)](https://packagist.org/packages/dereuromark/cakephp-tinyauth) [![Coding Standards](https://img.shields.io/badge/cs-PSR--2--R-yellow.svg)](https://github.com/php-fig-rectified/fig-rectified-standards) -A CakePHP 3.x plugin to handle user authorization the easy way. +A CakePHP 3.x plugin to handle authentication and user authorization the easy way. ## Demo +See http://sandbox3.dereuromark.de/auth-sandbox + +### auth-allow.ini +```ini +Users = index,view +PluginName.SomeController = * +``` + +### acl.ini ```ini [Users] index = * -add,edit = user, mod -* = admin +add,edit = user,mod [admin/Users] * = admin ``` -See http://sandbox3.dereuromark.de/auth-sandbox - ## How to include -Installing the plugin is pretty much as with every other CakePHP Plugin. +Installing the plugin is pretty much as with every other CakePHP plugin: ```bash -composer require dereuromark/cakephp-tinyauth:dev-master +composer require dereuromark/cakephp-tinyauth ``` Then, to load the plugin either run the following command: @@ -45,7 +51,7 @@ Plugin::load('TinyAuth'); That's it. It should be up and running. ## Docs -See [Docs](/docs). +For setup and usage see [Docs](/docs). Also note the original [blog post](http://www.dereuromark.de/2011/12/18/tinyauth-the-fastest-and-easiest-authorization-for-cake2/) and how it all started. diff --git a/composer.json b/composer.json index c4986fcd..1f03a33b 100644 --- a/composer.json +++ b/composer.json @@ -13,8 +13,8 @@ } ], "require": { - "php": ">=5.4", - "cakephp/cakephp": "~3.0" + "php": ">=5.5", + "cakephp/cakephp": "~3.2" }, "require-dev": { "fig-r/psr2r-sniffer": "dev-master" @@ -30,7 +30,8 @@ "autoload-dev": { "psr-4": { "TinyAuth\\Test\\": "tests", - "Cake\\Test\\": "vendor/cakephp/cakephp/tests" + "Cake\\Test\\": "vendor/cakephp/cakephp/tests", + "TestApp\\": "tests/TestApp" } }, "extra": { diff --git a/config/auth-allow.default.ini b/config/auth-allow.default.ini new file mode 100644 index 00000000..92d402c8 --- /dev/null +++ b/config/auth-allow.default.ini @@ -0,0 +1,5 @@ +Users = index, view ; Everyone can access index and view actions +admin/Users = index ; Only index action is public for admin prefix +Extras.Offers = * ; All Offers controller actions in Extras plugin are public + +; ... diff --git a/docs/Authentication.md b/docs/Authentication.md new file mode 100644 index 00000000..be53622e --- /dev/null +++ b/docs/Authentication.md @@ -0,0 +1,93 @@ +# TinyAuth Authentication +The fast and easy way for user authentication in CakePHP 3.x applications. + +Use TinyAuth Componont if you want to add instant (and easy) action whitelisting to your application. + +## Basic Features +- INI file (static) based access rights (controller-action setup) +- Lightweight and incredibly fast + +Do NOT use if +- you want to dynamically adjust access rights (or enhance it with a web +frontend yourself) + +## Enabling + +Authentication is set up in your controller's `initialize` method: + +```php +// src/Controller/AppController + +public function initialize() { + $this->loadComponent('TinyAuth.Auth', [ + 'filePath' => ... + ]); +} +``` + +## auth-allow.ini + +TinyAuth expects an ``auth-allow.ini`` file in your config directory. +Use it to specify what actions are not protected by authentication. + +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 action names +- the ``*`` wildcard to allow access to all actions of that controller + +```ini +; ---------------------------------------------------------- +; UsersController +; ---------------------------------------------------------- +Users = index +; ---------------------------------------------------------- +; UsersController using /api prefixed route +; ---------------------------------------------------------- +api/Users = index, view, edit +; ---------------------------------------------------------- +; UsersController using /admin prefixed route +; ---------------------------------------------------------- +admin/Users = * +; ---------------------------------------------------------- +; AccountsController in plugin named Accounts +; ---------------------------------------------------------- +Accounts.Accounts = view, edit +; ---------------------------------------------------------- +; AccountsController in plugin named Accounts using /admin +; prefixed route +; ---------------------------------------------------------- +Accounts.admin/Accounts = index +``` + +## Caching + +TinyAuth makes heavy use of caching to achieve optimal performance. + +You may however want to disable caching while developing to prevent +confusing (outdated) results. + +To disable caching either: + +- pass ``true`` to the ``autoClearCache`` configuration option +- use the example below to disable caching automatically for CakePHP debug mode + +```php +$this->loadComponent('TinyAuth.Auth', [ + 'autoClearCache' => Configure::read('debug') +)] +``` + +## Configuration + +TinyAuth AuthComponent supports the following configuration options. + +Option | Type | Description +:----- | :--- | :---------- +autoClearCache|bool|True will generate a new ACL cache file every time. +filePath|string|Full path to the acl.ini. Defaults to `ROOT . DS . 'config' . DS`. +file|string|Name of the INI file. Defaults to `auth-allow.ini`. +cache|string|Cache type. Defaults to `_cake_core_`. +cacheKey|string|Cache key. Defaults to `tiny_auth_allow`. + diff --git a/docs/Authorization.md b/docs/Authorization.md new file mode 100644 index 00000000..23666014 --- /dev/null +++ b/docs/Authorization.md @@ -0,0 +1,235 @@ +# TinyAuth Authorization +The fast and easy way for user authorization in CakePHP 3.x applications. + +Enable TinyAuth Authorize adapter if you want to add instant (and easy) role +based access control (RBAC) to your application. + +## Basic Features +- Single or multi role +- DB (dynamic) or Configure based role definition +- INI file (static) based access rights (controller-action/role setup) +- Lightweight and incredibly fast + +Do NOT use if +- you need ROW based access +- you want to dynamically adjust access rights (or enhance it with a web +frontend yourself) + +## Enabling + +Assuming you already have authentication set up correctly you can enable +authorization in your controller's `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' => [ + 'multiRole' => false + ] + ] + ]); +} +``` + +> Please note that TinyAuth Authorize can be used in combination with any +> [CakePHP Authentication Type](http://book.cakephp.org/3.0/en/controllers/components/authentication.html#choosing-an-authentication-type). + +## Roles + +TinyAuth requires the presence of roles to function so create those first using +one of the following two options. + +### Configure based roles + +Define your roles in a Configure array if you want to prevent database role +lookups, for example: + +```php +// config/app_custom.php + +/** +* Optionally define constants for easy referencing throughout your code +*/ +define('ROLE_USER', 1); +define('ROLE_ADMIN', 2); +define('ROLE_SUPERADMIN', 9); + +return [ + 'Roles' => [ + 'user' => ROLE_USER, + 'admin' => ROLE_ADMIN, + 'superadmin' => ROLE_SUPERADMIN + ] +]; +``` + +### Database roles +When you choose to store your roles in the database TinyAuth expects a table +named ``roles``. If you prefer to use another table name simply specify it using the +``rolesTable`` configuration option. + +>**Note:** make sure to add an "alias" field to your roles table (used as slug +identifier in the acl.ini file) + +Example of a record from a valid roles table: + +```php +'id' => '11' +'name' => 'User' +'description' => 'Basic authenticated user' +'alias' => 'user' +'created' => '2010-01-07 03:36:33' +'modified' => '2010-01-07 03:36:33' +``` + +> Please note that you do NOT need Configure based roles when using database +> roles. Also make sure to remove (or rename) existing Configure based roles +> since TinyAuth will always first try to find a matching Configure roles array +> before falling back to using the database. + +## Users + +### Single-role + +When using the single-role-per-user model TinyAuth expects your Users model to +contain an column named ``role_id``. If you prefer to use another column name +simply specify it using the ``roleColumn`` configuration option. + +The ``roleColumn`` option is also used on pivot table in a multi-role setup. + +### Multi-role +When using the multiple-roles-per-user model: + +- your database MUST have a ``roles`` table +- your database MUST have a valid join table (e.g. ``roles_users``). This can be overridden with the ``pivotTable`` option. +- the configuration option ``multiRole`` MUST be set to ``true`` + +Example of a record from a valid join table: + +```php +'id' => 1 +'user_id' => 1 +'role_id' => 1 +``` + +If you want to have default database tables here for multi-role auth, you can use the plugin shipped Migrations file: +``` +bin/cake migrations migrate -p TinyAuth +``` +Alternatively you can copy and paste this migration file to your `app/Config` folder and adjust the fields and table names and then use that modified version instead. + +## acl.ini + +TinyAuth 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 +``` + +## Caching + +TinyAuth makes heavy use of caching to achieve optimal performance. +By default it will not use caching in debug mode, though. + +You may however want to disable caching while developing RBAC to prevent +confusing (outdated) results. + +To disable caching either: + +- pass ``true`` to the ``autoClearCache`` configuration option +- use the example below to disable caching automatically for CakePHP debug mode + +```php +'TinyAuth' => [ + 'autoClearCache' => Configure::read('debug') +] +``` + +## Configuration + +TinyAuthorize adapter supports the following configuration options. + +Option | Type | Description +:----- | :--- | :---------- +roleColumn|string|Name of column in user table holding role id (used for foreign key in users table in a single role per user setup, or in the pivot table on multi-roles setup) +userColumn|string|Name of column in pivot table holding role id (only used in pivot table on multi-roles setup) +aliasColumn|string|Name of the column for the alias in the role table +idColumn|string|Name of the ID Column in users table +rolesTable|string|Name of Configure key holding all available roles OR class name of roles database table +usersTable|string|Class name of the users table. +pivotTable|string|Name of the pivot table, for a multi-group setup. +rolesTablePlugin|string|Name of the plugin for the roles table, if any. +pivotTablePlugin|string|Name of the plugin for the pivot table, if any. +multiRole|bool|True will enable multi-role/HABTM authorization (requires a valid join table) +superAdminRole|int|Id of the super admin role. Users with this role will have access to ALL resources. +superAdmin|int or string|Id/name of the super admin. Users with this id/name will have access to ALL resources.null/0/"0" take disable it +superAdminColumn|string|Column of super admin in user table.default use idColumn option +authorizeByPrefix|bool|If prefixed routes should be auto-handled by their matching role name. +prefixes|array|A list of authorizeByPrefix handled prefixes. +allowUser|bool|True will give authenticated users access to all resources except those using the `adminPrefix` +adminPrefix|string|Name of the prefix used for admin pages. Defaults to admin. +autoClearCache|bool|True will generate a new ACL cache file every time. +filePath|string|Full path to the acl.ini. Defaults to `ROOT . DS . 'config' . DS`. +file|string|Name of the INI file. Defaults to `acl.ini`. +cache|string|Cache type. Defaults to `_cake_core_`. +cacheKey|string|Cache key. Defaults to `tiny_auth_acl`. + + +## Auth user data +For reading auth user data take a look at [Tools plugin AuthUser component/helper](https://github.com/dereuromark/cakephp-tools/blob/master/docs/Auth/Auth.md). diff --git a/docs/README.md b/docs/README.md index 8cfd205c..de7b9af7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,231 +1,30 @@ -# TinyAuth Authorization -The fast and easy way for user authorization in CakePHP 3.x applications. +# TinyAuth Authentication and Authorization -Enable TinyAuth Authorize adapter if you want to add instant (and easy) role -based access (RBAC) to your application. +This plugin ships with both +- Authentication: Always comes first - "who is it"? +- Authorization: Afterwards - "What can this person see"? -## Basic Features -- Single or multi role -- DB (dynamic) or Configure based role definition -- INI file (static) based access rights (controller-action/role setup) -- Lightweight and incredibly fast +For the first one usually declares as a whitelist of actions per controller that will not require any authentication. +If an action is not whitelisted, it will trigger the login process. -Do NOT use if -- you need ROW based access -- you want to dynamically adjust access rights (or enhance it with a web -frontend yourself) +The second only gets invoked once the person is already logged in. +In that case the role of this logged in user decides if that action is allowed to be accessed. -## Enabling +## Authentication +NEW since version 1.4.0 -Assuming you already have authentication set up correctly you can enable -authorization in your controller's `beforeFilter` like this example: +This is done via TinyAuth Auth Component. -```php -// src/Controller/AppController +The component plays well together with the adapter (see below). +If you do not have any roles and either all are logged in or not logged in you can also use this stand-alone to make certain pages public. -use Cake\Event\Event; +See [Authentication](Authentication.md) docs. -public function beforeFilter(Event $event) { - parent::beforeFilter($event); +## Authorization +For this we have a TinyAuth Authorize adapter. - $this->loadComponent('Auth', [ - 'authorize' => [ - 'TinyAuth.Tiny' => [ - 'multiRole' => false - ] - ] - ]); -} -``` +The adapter plays well together with the component above. +But if you prefer to control the action whitelisting for authentication via code and `$this->Auth->allow()` calls, you can +also just use this adapter stand-alone for the ACL part of your application. -> Please note that TinyAuth Authorize can be used in combination with any -> [CakePHP Authentication Type](http://book.cakephp.org/3.0/en/controllers/components/authentication.html#choosing-an-authentication-type). - -## Roles - -TinyAuth requires the presence of roles to function so create those first using -one of the following two options. - -### Configure based roles - -Define your roles in a Configure array if you want to prevent database role -lookups, for example: - -```php -// config/app_custom.php - -/** -* Optionally define constants for easy referencing throughout your code -*/ -define('ROLE_USER', 1); -define('ROLE_ADMIN', 2); -define('ROLE_SUPERADMIN', 9); - -return [ - 'Roles' => [ - 'user' => ROLE_USER, - 'admin' => ROLE_ADMIN, - 'superadmin' => ROLE_SUPERADMIN - ] -]; -``` - -### Database roles -When you choose to store your roles in the database TinyAuth expects a table -named ``roles``. If you prefer to use another table name simply specify it using the -``rolesTable`` configuration option. - ->**Note:** make sure to add an "alias" field to your roles table (used as slug -identifier in the acl.ini file) - -Example of a record from a valid roles table: - -```php -'id' => '11' -'name' => 'User' -'description' => 'Basic authenticated user' -'alias' => 'user' -'created' => '2010-01-07 03:36:33' -'modified' => '2010-01-07 03:36:33' -``` - -> Please note that you do NOT need Configure based roles when using database -> roles. Also make sure to remove (or rename) existing Configure based roles -> since TinyAuth will always first try to find a matching Configure roles array -> before falling back to using the database. - -## Users - -### Single-role - -When using the single-role-per-user model TinyAuth expects your Users model to -contain an column named ``role_id``. If you prefer to use another column name -simply specify it using the ``roleColumn`` configuration option. - -The ``roleColumn`` option is also used on pivot table in a multi-role setup. - -### Multi-role -When using the multiple-roles-per-user model: - -- your database MUST have a ``roles`` table -- your database MUST have a valid join table (e.g. ``roles_users``). This can be overridden with the ``pivotTable`` option. -- the configuration option ``multiRole`` MUST be set to ``true`` - -Example of a record from a valid join table: - -```php -'id' => 1 -'user_id' => 1 -'role_id' => 1 -``` - -If you want to have default database tables here for multi-role auth, you can use the plugin shipped Migrations file: -``` -bin/cake migrations migrate -p TinyAuth -``` -Alternatively you can copy and paste this migration file to your `app/Config` folder and adjust the fields and table names and then use that modified version instead. - -## acl.ini - -TinyAuth 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 -``` - -## Caching - -TinyAuth makes heavy use of caching to achieve optimal performance. - -You may however want to disable caching while developing RBAC to prevent -confusing (outdated) results. - -To disable caching either: - -- pass ``true`` to the ``autoClearCache`` configuration option -- use the example below to disable caching automatically for CakePHP debug mode - -```php -'TinyAuth' => [ - 'autoClearCache' => Configure::read('debug') -] -``` - -## Configuration - -TinyAuth supports the following configuration options. - -Option | Type | Description -:----- | :--- | :---------- -roleColumn|string|Name of column in user table holding role id (used for foreign key in users table in a single role per user setup, or in the pivot table on multi-roles setup) -userColumn|string|Name of column in pivot table holding role id (only used in pivot table on multi-roles setup) -aliasColumn|string|Name of the column for the alias in the role table -idColumn|string|Name of the ID Column in users table -rolesTable|string|Name of Configure key holding all available roles OR class name of roles database table -usersTable|string|Class name of the users table. -pivotTable|string|Name of the pivot table, for a multi-group setup. -rolesTablePlugin|string|Name of the plugin for the roles table, if any. -pivotTablePlugin|string|Name of the plugin for the pivot table, if any. -multiRole|bool|True will enable multi-role/HABTM authorization (requires a valid join table) -superAdminRole|int|Id of the super admin role. Users with this role will have access to ALL resources. -superAdmin|int or string|Id/name of the super admin. Users with this id/name will have access to ALL resources.null/0/"0" take disable it -superAdminColumn|string|Column of super admin in user table.default use idColumn option -authorizeByPrefix|bool|If prefixed routes should be auto-handled by their matching role name. -prefixes|array|A list of authorizeByPrefix handled prefixes. -allowUser|bool|True will give authenticated users access to all resources except those using the `adminPrefix` -adminPrefix|string|Name of the prefix used for admin pages. Defaults to admin. -autoClearCache|bool|True will generate a new ACL cache file every time. -aclPath|string|Full path to the acl.ini. Defaults to `ROOT . DS . 'config' . DS`. - - -## Auth user data -For reading auth user data take a look at [Tools plugin AuthUser component/helper](https://github.com/dereuromark/cakephp-tools/blob/master/docs/Auth/Auth.md). +See [Authorization](Authorization.md) docs. diff --git a/src/Auth/TinyAuthorize.php b/src/Auth/TinyAuthorize.php index 83c3d289..9e1dd232 100644 --- a/src/Auth/TinyAuthorize.php +++ b/src/Auth/TinyAuthorize.php @@ -8,15 +8,16 @@ use Cake\Core\Exception\Exception; use Cake\Network\Request; use Cake\ORM\TableRegistry; +use TinyAuth\Utility\Utility; /** - * @deprecated Directly use configs + * @deprecated Directly use cache config key */ if (!defined('AUTH_CACHE')) { define('AUTH_CACHE', '_cake_core_'); // use the most persistent cache by default } /** - * @deprecated Directly use configs + * @deprecated Directly use file config key */ if (!defined('ACL_FILE')) { define('ACL_FILE', 'acl.ini'); // stored in /config/ by default @@ -66,10 +67,12 @@ class TinyAuthorize extends BaseAuthorize { 'prefixes' => [], // Whitelisted prefixes (only used when allowAdmin is enabled), leave empty to use all available 'allowUser' => false, // enable to allow ALL roles access to all actions except prefixed with 'adminPrefix' 'adminPrefix' => 'admin', // name of the admin prefix route (only used when allowUser is enabled) - 'cache' => AUTH_CACHE, + 'cache' => '_cake_core_', 'cacheKey' => 'tiny_auth_acl', - 'autoClearCache' => false, // usually done by Cache automatically in debug mode, - 'aclPath' => null, // possible to locate acl.ini at given path e.g. Plugin::configPath('Admin') + 'autoClearCache' => false, // Set to true to delete cache automatically in debug mode + 'aclPath' => null, // @deprecated Use filePath + 'filePath' => null, // Possible to locate ini file at given path e.g. Plugin::configPath('Admin') + 'file' => 'acl.ini', ]; /** @@ -89,6 +92,11 @@ public function __construct(ComponentRegistry $registry, array $config = []) { if (!in_array($config['cache'], Cache::configured())) { throw new Exception(sprintf('Invalid TinyAuthorization cache `%s`', $config['cache'])); } + + // BC only + if (isset($this->_config['aclPath'])) { + $this->_config['filePath'] = $this->_config['aclPath']; + } } /** @@ -214,12 +222,12 @@ protected function _getAcl($path = null) { return $roles; } - $iniArray = $this->_parseFile($path . ACL_FILE); + $iniArray = $this->_parseFile($path . $this->_config['file']); $availableRoles = $this->_getAvailableRoles(); $res = []; foreach ($iniArray as $key => $array) { - $res[$key] = $this->_deconstructIniKey($key); + $res[$key] = Utility::deconstructIniKey($key); $res[$key]['map'] = $array; foreach ($array as $actions => $roles) { @@ -272,23 +280,9 @@ protected function _getAcl($path = null) { * * @param string $ini Full path to the acl.ini file * @return array List with all available roles - * @throws \Cake\Core\Exception\Exception */ protected function _parseFile($ini) { - if (!file_exists($ini)) { - throw new Exception(sprintf('Missing TinyAuthorize ACL file (%s)', $ini)); - } - - if (function_exists('parse_ini_file')) { - $iniArray = parse_ini_file($ini, true); - } else { - $iniArray = parse_ini_string(file_get_contents($ini), true); - } - - if (!is_array($iniArray)) { - throw new Exception('Invalid TinyAuthorize ACL file'); - } - return $iniArray; + return Utility::parseFile($ini); } /** @@ -298,19 +292,7 @@ protected function _parseFile($ini) { * @return array Array 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; + return Utility::deconstructIniKey($key); } /** @@ -351,7 +333,7 @@ protected function _getAvailableRoles() { })->toArray(); if (count($roles) < 1) { - throw new Exception('Invalid TinyAuthorize role setup (roles table `' . $this->_config['rolesTable'] . '` has no roles)'); + throw new Exception('Invalid TinyAuth role setup (roles table `' . $this->_config['rolesTable'] . '` has no roles)'); } return $roles; } @@ -374,7 +356,7 @@ protected function _getUserRoles($user) { if (isset($user[$this->_config['roleColumn']])) { return [$user[$this->_config['roleColumn']]]; } - throw new Exception(sprintf('Missing TinyAuthorize role id (%s) in user session', $this->_config['roleColumn'])); + throw new Exception(sprintf('Missing TinyAuth role id (%s) in user session', $this->_config['roleColumn'])); } // Multi-role case : load the pivot table @@ -399,7 +381,7 @@ protected function _getUserRoles($user) { ->toArray(); if (!count($roles)) { - throw new Exception('Missing TinyAuthorize roles for user in pivot table'); + throw new Exception('Missing TinyAuth roles for user in pivot table'); } return $roles; } diff --git a/src/Controller/Component/AuthComponent.php b/src/Controller/Component/AuthComponent.php new file mode 100644 index 00000000..0e938cf9 --- /dev/null +++ b/src/Controller/Component/AuthComponent.php @@ -0,0 +1,130 @@ + '_cake_core_', + 'cacheKey' => 'tiny_auth_allow', + 'autoClearCache' => false, // Set to true to delete cache automatically in debug mode + 'filePath' => null, // Possible to locate ini file at given path e.g. Plugin::configPath('Admin') + 'file' => 'auth-allow.ini', + ]; + + /** + * @param \Cake\Controller\ComponentRegistry $registry + * @param array $config + * @throws \Cake\Core\Exception\Exception + */ + public function __construct(ComponentRegistry $registry, array $config = []) { + $config += $this->_defaultTinyAuthConfig; + + parent::__construct($registry, $config); + + if (!in_array($config['cache'], Cache::configured())) { + throw new Exception(sprintf('Invalid TinyAuth cache `%s`', $config['cache'])); + } + } + + /** + * @param \Cake\Event\Event $event Event instance. + * @return \Cake\Network\Response|null + */ + public function startup(Event $event) { + $this->_prepareAuthentication(); + + return parent::startup($event); + } + + /** + * @return void + */ + protected function _prepareAuthentication() { + $authentication = $this->_getAuth($this->_config['filePath']); + + $params = $this->request->params; + foreach ($authentication as $rule) { + if ($params['plugin'] && $params['plugin'] !== $rule['plugin']) { + continue; + } + if (!empty($params['prefix']) && $params['prefix'] !== $rule['prefix']) { + continue; + } + if ($params['controller'] !== $rule['controller']) { + continue; + } + + if ($rule['actions'] === []) { + $this->allow(); + return; + } + + $this->allow($rule['actions']); + } + } + + /** + * Parse ini file and returns the allowed actions. + * + * Uses cache for maximum performance. + * + * @param string|null $path + * @return array Actions + */ + protected function _getAuth($path = null) { + if ($path === null) { + $path = ROOT . DS . 'config' . DS; + } + + if ($this->_config['autoClearCache'] && Configure::read('debug')) { + Cache::delete($this->_config['cacheKey'], $this->_config['cache']); + } + $roles = Cache::read($this->_config['cacheKey'], $this->_config['cache']); + if ($roles !== false) { + return $roles; + } + + $iniArray = Utility::parseFile($path . $this->_config['file']); + + $res = []; + foreach ($iniArray as $key => $actions) { + $res[$key] = Utility::deconstructIniKey($key); + $res[$key]['map'] = $actions; + + $actions = explode(',', $actions); + + if (in_array('*', $actions)) { + $res[$key]['actions'] = []; + continue; + } + + foreach ($actions as $action) { + $action = trim($action); + if (!$action) { + continue; + } + + $res[$key]['actions'][] = $action; + } + } + + Cache::write($this->_config['cacheKey'], $res, $this->_config['cache']); + return $res; + } + +} diff --git a/src/Utility/Utility.php b/src/Utility/Utility.php new file mode 100644 index 00000000..5b3fbb62 --- /dev/null +++ b/src/Utility/Utility.php @@ -0,0 +1,54 @@ + 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; + } + + /** + * Returns the ini file as an array. + * + * @param string $ini Full path to the ini file + * @return array List + * @throws \Cake\Core\Exception\Exception + */ + public static function parseFile($ini) { + if (!file_exists($ini)) { + throw new Exception(sprintf('Missing TinyAuth authentication file (%s)', $ini)); + } + + if (function_exists('parse_ini_file')) { + $iniArray = parse_ini_file($ini, true); + } else { + $iniArray = parse_ini_string(file_get_contents($ini), true); + } + + if (!is_array($iniArray)) { + throw new Exception('Invalid TinyAuth authentication file'); + } + return $iniArray; + } + +} diff --git a/tests/TestApp/Controller/OffersController.php b/tests/TestApp/Controller/OffersController.php new file mode 100644 index 00000000..1e84821c --- /dev/null +++ b/tests/TestApp/Controller/OffersController.php @@ -0,0 +1,14 @@ +componentConfig = [ + 'filePath' => Plugin::path('TinyAuth') . 'tests' . DS . 'test_files' . DS, + 'autoClearCache' => true, + ]; + } + + /** + * @return void + */ + public function testValid() { + $request = new Request(['params' => [ + 'controller' => 'Users', + 'action' => 'view', + 'plugin' => null, + '_ext' => null, + 'pass' => [1] + ]]); + $controller = $this->getControllerMock($request); + + $registry = new ComponentRegistry($controller); + $this->AuthComponent = new AuthComponent($registry, $this->componentConfig); + + $config = []; + $this->AuthComponent->initialize($config); + + $event = new Event('Controller.startup', $controller); + $response = $this->AuthComponent->startup($event); + $this->assertNull($response); + } + + /** + * @return void + */ + public function testValidAnyAction() { + $request = new Request(['params' => [ + 'controller' => 'Offers', + 'action' => 'index', + 'plugin' => 'Extras', + '_ext' => null, + 'pass' => [1] + ]]); + $controller = new OffersController($request); + + $registry = new ComponentRegistry($controller); + $this->AuthComponent = new AuthComponent($registry, $this->componentConfig); + + $config = []; + $this->AuthComponent->initialize($config); + + $event = new Event('Controller.startup', $controller); + $response = $this->AuthComponent->startup($event); + $this->assertNull($response); + } + + /** + * @return void + */ + public function testInvalid() { + $request = new Request(['params' => [ + 'controller' => 'FooBar', + 'action' => 'index', + 'plugin' => null, + '_ext' => null, + 'pass' => [] + ]]); + $controller = $this->getControllerMock($request); + + $registry = new ComponentRegistry($controller); + $this->AuthComponent = new AuthComponent($registry, $this->componentConfig); + + $config = []; + $this->AuthComponent->initialize($config); + + $event = new Event('Controller.startup', $controller); + $response = $this->AuthComponent->startup($event); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame(302, $response->statusCode()); + } + + /** + * @param \Cake\Network\Request $request + * @return \Cake\Controller\Controller + */ + protected function getControllerMock(Request $request) { + $controller = $this->getMockBuilder(Controller::class) + ->setConstructorArgs([$request]) + ->setMethods(['isAction']) + ->getMock(); + + $controller->expects($this->once())->method('isAction')->willReturn(true); + + return $controller; + } + +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index ac6f82e1..f4649478 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -55,7 +55,6 @@ Cake\Cache\Cache::config($cache); -//needed? Cake\Core\Plugin::load('TinyAuth', ['path' => ROOT . DS, 'autoload' => true]); // Ensure default test connection is defined diff --git a/tests/config/routes.php b/tests/config/routes.php new file mode 100644 index 00000000..647d8346 --- /dev/null +++ b/tests/config/routes.php @@ -0,0 +1,9 @@ +fallbacks(DashedRoute::class); +}); diff --git a/tests/test_files/auth-allow.ini b/tests/test_files/auth-allow.ini new file mode 100644 index 00000000..92d402c8 --- /dev/null +++ b/tests/test_files/auth-allow.ini @@ -0,0 +1,5 @@ +Users = index, view ; Everyone can access index and view actions +admin/Users = index ; Only index action is public for admin prefix +Extras.Offers = * ; All Offers controller actions in Extras plugin are public + +; ...