diff --git a/README.md b/README.md index 6600563..d9cd32a 100644 --- a/README.md +++ b/README.md @@ -1,123 +1,147 @@ -#Access control library for the Lithium framework. +# Access control library for the Lithium framework. -##Installation +## Installation Checkout the code to either of your library directories: - cd libraries - git clone git@github.com + cd libraries + git clone https://github.com/tmaiaroto/li3_access.git Include the library in in your `/app/config/bootstrap/libraries.php` - Libraries::add('li3_access'); + Libraries::add('li3_access'); -##Usage +## Usage You must configure the adapter you wish to use first, but once you have it configured it's fairly simple to use. - $access = Access::check('access_config_name', Auth::check('auth_config_name'), $this->request); - if(!empty($access)) { - $this->redirect($access['redirect']); - } + $access = Access::check('access_config_name', $this->request, Auth::check('auth_config_name')); + if(!empty($access)) { + $this->redirect($access['redirect']); + } -If the request validates correctly based on your configuration then `Access::check()` will return an empty `array()` otherwise it will return and array with two keys; `message` and `redirect`. These values are built into the Access class but you can override them but passing them as `$options` to all three of the adapters in this repository. +If the request validates correctly based on your configuration then `Access::check()` will return an empty `array()` otherwise it will return an array with two keys; `message` and `redirect`. These values are built into the Access class but you can override them by passing them as `$options` to all three of the adapters in this repository. -##Configuration +## Configuration In this repository there are three adapters. All three work in a slightly different way. -###Simple Adapter +### Simple Adapter The simple adapter is exactly what it says it is. The check method only checks that the data passed to is not empty and as a result the configuration is trivial. - Access::config( - 'simple' => array('adapter' => 'Simple') - ); + Access::config( + 'simple' => array('adapter' => 'Simple') + ); And that's it! -###Rules Adapter +### Rules Adapter This adapter effectively allows you to tell it how it should work. It comes with a few preconfigured rules by default but it's very simple to add your own. Its configuration is the same as the `Simple` adapter if you only want to use the built in methods. - Access::config( - 'rules' => array('adapter' => 'Rules') - ); + Access::config( + 'rules' => array('adapter' => 'Rules') + ); Then to deny all requests from the authenticated user. - $access = Access::check('rules', Auth::check('auth_config_name'), $this->request, array('rule' => 'denyAll')); - if(!empty($access)) { - $this->redirect($access['redirect']); - } + $access = Access::check('rules', Auth::check('auth_config_name'), $this->request, array('rule' => 'denyAll')); + if(!empty($access)) { + $this->redirect($access['redirect']); + } -There are four built in rules; allowAll, denyAll, allowAnyUser and allowIp, for more information see the adapter itself. However, this adapter is at it's most useful when you add your own rules. +There are four built in rules; allowAll, denyAll, allowAnyUser and allowIp, for more information see the adapter itself. However, this adapter is at its most useful when you add your own rules. - Access::adapter('custom_rule')->add(function($user, $request, $options) { - // Your logic here. Just make sure it returns an array. - }); + Access::adapter('custom_rule')->add(function($user, $request, $options) { + // Your logic here. Just make sure it returns an array. + }); Then to use your new rule: - $access = Access::check('rules', Auth::check('auth_config_name'), $this->request, array('rule' => 'custom_rule')); + $access = Access::check('rules', Auth::check('auth_config_name'), $this->request, array('rule' => 'custom_rule')); One more to go! -###AuthRbac Adapter +### AuthRbac Adapter This is the most complex adapter in this repository at this time. It's used for Role Based Access Control. You define a set of roles (or conditions) to match the request against, if the request matches your conditions the adapter then checks to see if the user is authenticated with the appropriate `\lithium\security\Auth` configurations to be granted access. -It's difficult to explain (I hope that's clear enough) so lets look at an example configuration to try and achive some clarity: - - Access::config( - 'auth_rbac' => array( - 'adapter' => 'AuthRbac', - 'roles' => array( - array( - 'requesters' => '*', - 'match' => '*::*' - ), - array( - 'message' => 'No panel for you!', - 'redirect' => array('library' => 'admin', 'Users::login'), - 'requesters' => 'admin', - 'match' => array('library' => 'admin', '*::*') - ), - array( - 'requesters' => '*', - 'match' => array('library' => 'admin', 'Users::login') - ), - array( - 'requesters' => '*', - 'match' => array('library' => 'admin', 'Users::logout') - ) - ) - ) - ) +It's difficult to explain (I hope that's clear enough) so lets look at an example configuration to try and achieve some clarity: + + $accountsEmpty = Accounts::count(); + + Access::config(array( + 'auth_rbac' => array( + 'adapter' => 'AuthRbac', + 'roles' => array( + array( + 'resources' => '*', + 'match' => '*::*' + ), + array( + 'message' => 'No panel for you!', + 'redirect' => array('library' => 'admin', 'Users::login'), + 'resources' => 'admin', + 'match' => array('library' => 'admin', '*::*') + ), + array( + 'resources' => '*', + 'match' => array( + 'library' => 'admin', 'Users::login', + function($request, &$options) { + return !empty($request->data); + } + ), + 'allow' => function($request, &$options) use ($accountsEmpty) { + if ($accountsEmpty) { + $options['message'] = 'No accounts exist yet!'; + } + return $accountsEmpty; + } + ), + array( + 'resources' => '*', + 'match' => array('library' => 'admin', 'Users::logout') + ) + ) + ) + )); First we tell it which adapter to use: - 'adapter' => 'AuthRbac', + 'adapter' => 'AuthRbac', Then we set the roles array. This array is required if you want to use this adapter. The roles are evaluated from top to bottom. So if a role at the bottom contradicts one closer to the top, the bottom will take precedence. -####There are five possible options you can specify for a single role. +#### There are five possible options you can specify for a single role. -*match* +`'message'` -A rule used to match (see: `AuthRbac::parseMatch()`) this role against the request object passed from the `check()` method. You may use a parameters array where you explicitly set the parameter/value pairs, a shorthand syntax very similar to the one you use when generating urls or even a. Without match being set the role will always deny access. +Overwrites the default message to display if the rule matches the request and is disallowed. -Examples: +`'redirect'` -* `'Dashboards::index'` -> `array('controller' => 'Dashboards', 'action' => 'index')` -* `'Dashboards::*'` -> `array('controller' => 'Dashboards', 'action' => '*')` -> `Any action in the Dasboards controller.` -* `array('library' => 'admin', '*::*');` -> `array('library' => 'admin_plugin', 'controller' => '*', 'action' => '*')` -> `Any controller/action combination under the admin library.` +Overwrites the default redirect to use if the rule matches the request and is dissallowed. -**requester** +`'match'` -A string or an array of auth configuration keys that this rule applies to. The string `*` denotes everyone, even those who are not authenticated. A string of `admin` will apply this to everyone who can be authenticated against the user defined `admin` Auth configuration. An array of configuration keys does the same but you can apply it to multiple Auth configurations in one go. +A rule used to match this role against the request object passed from the `check()` method. You may use a parameters array where you explicitly set the parameter/value pairs, a shorthand syntax very similar to the one you use when generating urls or even a closure. Without match being set the role will always deny access. -*Example*: +In the closure example configuration: + + 'match' => array( + 'library' => 'admin', 'Users::login', + function($request, &$roleOptions) { + return !empty($request->data); + } + ) + +Not only must the library, controller and action match but the closure must return true. So this role will only apply to this request if all of the request params match and the request data is set. + +`'resources'` + +A string or an array of auth configuration keys that this rule applies to. The string `*` denotes everyone, even those who are not authenticated. A string of `admin` will validate anyone who can be authenticated against the user defined `admin` Auth configuration. An array of configuration keys does the same but you can apply it to multiple Auth configurations in one go. Assuming we have an Auth configuration like so: @@ -142,31 +166,33 @@ Assuming we have an Auth configuration like so: ) )); -Setting 'requester' => array('user', 'customer') would only apply the rule to anyone that could authenticate as a user or customer. Setting 'requester' => '*' would mean that all of these auth configurations and people that are not authenticated would have this role applied to them. +Setting `'resources' => array('user', 'customer')` would only apply the rule to anyone that could authenticate as a user or customer. Setting `'resource' => '*'` would mean that all of these auth configurations and people that are not authenticated would have this role applied to them. + +`'allow'` -**allow** +A boolean that if set to false forces a role that would have been granted access to deny access. Much like the 'match' option you can also pass a closure to this option. This way you can blacklist every resource and then whitelist resources manually. Also by passing a closure you can deny access based upon the request. -A boolean that if set to false forces a role that would have been granted access to deny access. This way you can apply a rule to everyone and then proceed to exclude requesters manualy. +Finally, if you pass either $request or $options you can modify their values at runtime. -###Filters +### Filters The Access::check() method is filterable. You can apply the filters in the configuration like so: - Access::config(array( - 'rule_based' => array( - 'adapter' => 'Rules', - 'filters' => array( - function($self, $params, $chain) { - // Filter logic goes here - return $chain->next($self, $params, $chain); - } - ) - ) - )); + Access::config(array( + 'rule_based' => array( + 'adapter' => 'Rules', + 'filters' => array( + function($self, $params, $chain) { + // Filter logic goes here + return $chain->next($self, $params, $chain); + } + ) + ) + )); -##Credits +## Credits -###Tom Maiaroto +### Tom Maiaroto The original author of this library. @@ -174,7 +200,7 @@ Github: [tmaiaroto](https://github.com/tmaiaroto/li3_access) Website: [Shift8 Creative](http://www.shift8creative.com) -##Weluse +## Weluse Wrote the original Rbac adapter. @@ -182,7 +208,7 @@ Github: [dgAlien](https://github.com/dgAlien/li3_access) [weluse](https://github Website: [Weluse](http://www.weluse.de) -##rich97 +## rich97 Modified the original Rbac adapter, added some tests and wrote this version of the documentation. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f5a5d4a --- /dev/null +++ b/composer.json @@ -0,0 +1,14 @@ +{ + "name": "ciaro/li3_access", + "description": "Lithium Access Plugin.", + "type": "lithium-library", + "authors": [ + { + "name": "Tom Maiaroto" + } + ], + "require": { + "composer/installers": "*" + }, + "minimum-stability": "dev" +} \ No newline at end of file diff --git a/extensions/adapter/security/access/AuthRbac.php b/extensions/adapter/security/access/AuthRbac.php index 2c065cb..4336448 100644 --- a/extensions/adapter/security/access/AuthRbac.php +++ b/extensions/adapter/security/access/AuthRbac.php @@ -3,16 +3,15 @@ namespace li3_access\extensions\adapter\security\access; use lithium\security\Auth; +use lithium\util\Inflector; use lithium\core\ConfigException; -use li3_access\security\Access; - class AuthRbac extends \lithium\core\Object { - /** - * @var array $_autoConfig - * @see lithium\core\Object::$_autoConfig - */ + /** + * @var array $_autoConfig + * @see lithium\core\Object::$_autoConfig + */ protected $_autoConfig = array('roles'); /** @@ -21,176 +20,256 @@ class AuthRbac extends \lithium\core\Object { protected $_roles = null; /** - * The `Rbac` adapter will iterate trough the rbac data Array. + * The `Rbac` adapter will iterate through the rbac data Array. * - * @param mixed $user The user data array that holds all necessary information about + * @todo: write better tests! + * + * @param mixed $requester The user data array that holds all necessary information about * the user requesting access. Or false (because Auth::check() can return false). - * This is an optional parameter, bercause we will fetch the users data trough Auth + * This is an optional parameter, because we will fetch the users data through Auth * seperately. - * @param object $request The Lithium Request object. + * @param mixed $params The Lithium `Request` object, or an array with at least + * 'request', and 'params' + * @param array $options An array of additional options for the _getRolesByAuth method. + * @return Array An empty array if access is allowed or + * an array with reasons for denial if denied. + */ + public function check($requester, $params, array $options = array()) { + if (empty($this->_roles)) { + throw new ConfigException('No roles defined for adapter configuration.'); + } + + $roleDefaults = array( + 'message' => '', + 'redirect' => '', + 'allow' => true, + 'requesters' => '*', + 'match' => '*::*' + ); + + $message = $options['message']; + $redirect = $options['redirect']; + + $accessible = false; + foreach ($this->_roles as $role) { + $role += $roleDefaults; + if (is_callable($role['allow'])) { + $role['allow'] = (array) $role['allow']; + } + + // Check to see if this role applies to this request + if (!static::parseMatch($role['match'], $params)) { + continue; + } + + $accessible = static::_isAccessible($role, $params, $options); + + if (!$accessible) { + $message = !empty($role['message']) ? $role['message'] : $message; + $redirect = !empty($role['redirect']) ? $role['redirect'] : $redirect; + } + } + + return !$accessible ? compact('message', 'redirect') : array(); + } + + /** + * Checks if the Role grants access + * If allow === false => no access + * If requesters has no role => no access + * If allows contains closures => return closures return + * Otherwise => grants access + * + * @param array $role Array Set of Roles (dereferenced) + * @param mixed $quest A lithium Request object. * @param array $options An array of additional options for the _getRolesByAuth method. - * @return Array An empty array if access is allowed and an array with reasons for denial if denied. + * @return boolean $accessable */ - public function check($requester, $request, array $options = array()) { - if (empty($this->_roles)) { - throw new ConfigException('No roles defined for adapter configuration.'); - } - - $roleDefaults = array( - 'message' => '', - 'redirect' => '', - 'allow' => true, - 'requesters' => '*', - 'match' => '*::*' - ); - - $message = $options['message']; - $redirect = $options['redirect']; - - $accessable = false; - foreach ($this->_roles as $role) { - $role += $roleDefaults; - - // Check to see if this role applies to this request - if (!static::parseMatch($role['match'], $request)) { - continue; - } - $accessable = true; - - if (($role['allow'] === false) || - (!static::_hasRole($role['requesters'], $request, $options)) || - (is_array($role['allow']) && !static::_parseClosures($role['allow'], $request, $role)) - ) { - $accessable = false; - } - - if (!$accessable) { - $message = !empty($role['message']) ? $role['message'] : $message; - $redirect = !empty($role['redirect']) ? $role['redirect'] : $redirect; - } - } - - return !$accessable ? compact('message', 'redirect') : array(); - } - - /** - * parseMatch Matches the current request parameters against a set of given parameters. - * Can match against a shorthand string (Controller::action) or a full array. If a parameter - * is provided then it must have an equivilent in the Request objects parmeters in order - * to validate. * Is also acceptable to match a parameter without a specific value. - * - * @param mixed $match A set of parameters to validate the request against. - * @param mixed $request A lithium Request object. - * @access public - * @return boolean True if a match is found. - */ - public static function parseMatch($match, $request) { - if (empty($match)) { - return false; - } - - if (is_array($match)) { - if (!static::_parseClosures($match, $request)) { - return false; - } - } - - $params = array(); - foreach ((array) $match as $key => $param) { - if (is_string($param)) { - if (preg_match('/^[A-Za-z0-9_\*]+::[A-Za-z0-9_\*]+$/', $param, $regexMatches)) { - list($controller, $action) = explode('::', reset($regexMatches)); - $params += compact('controller', 'action'); - continue; - } - } - - $params[$key] = $param; - } - - foreach ($params as $type => $value) { - if ($value === '*') { - continue; - } - - if ($type === 'controller') { - $value = \lithium\util\Inflector::underscore($value); - } - - if (!array_key_exists($type, $request->params) || $value !== $request->params[$type]) { - return false; - } - } - - return true; - } - - /** - * _parseClosures Itterates over an array and runs any anonymous functions it - * finds. Returns true if all of the closures it runs evaluate to true. $match - * is passed by refference and any closures found are removed from it before the - * method is complete. - * - * @param array $data - * @param mixed $request - * @static - * @access protected - * @return void - */ - protected static function _parseClosures(array &$data = array(), $request = null, array &$roleOptions = array()) { - $return = true; - foreach ($data as $key => $item) { - if (is_callable($item)) { - if ($return === true) { - $return = (boolean) $item($request, $roleOptions); - } - unset($data[$key]); - } - } - return $return; - } + protected static function _isAccessible(&$role, $params, $options) { + if ($role['allow'] === false) { + return false; + } + if (!static::_hasRole($role['requesters'], $params, $options)) { + return false; + } + if (is_array($role['allow'])) { + return static::_parseClosures($role['allow'], $params['request'], $role); + } + return true; + } + + /** + * parseMatch Matches the current request parameters against a set of given parameters. + * Can match against a shorthand string (Controller::action) or a full array. If a parameter + * is provided then it must have an equivilent in the Request objects parmeters in order + * to validate. * Is also acceptable to match a parameter without a specific value. + * + * @param mixed $match A set of parameters to validate the request against. + * @param mixed $params The Lithium `Request` object, or an array with at least + * 'request', and 'params' + * @access public + * @return boolean True if a match is found. + */ + public static function parseMatch($match, $params) { + if (empty($match)) { + return false; + } + + if (is_array($match)) { + $_params = $params; + if (!static::_parseClosures($match, $params['request'], $_params)) { + return false; + } + } elseif (is_callable($match)) { + return (boolean) $match($params['request'], $params); + } + + $matchParams = array(); + foreach ((array) $match as $key => $param) { + if (is_string($param)) { + if (preg_match('/^([A-Za-z0-9_\*\\\]+)::([A-Za-z0-9_\*]+)$/', $param, $regexMatches)) { + $matchParams += array( + 'controller' => $regexMatches[1], + 'action' => $regexMatches[2] + ); + continue; + } + } + + $matchParams[$key] = $param; + } + + foreach ($matchParams as $type => $value) { + if ($value === '*') { + continue; + } + + if ($type === 'controller') { + $value = Inflector::underscore($value); + } + + $exists_in_request = array_key_exists($type, $params['params']); + if (!$exists_in_request || $value !== Inflector::underscore($params['params'][$type])) { + return false; + } + } + return true; + } + + /** + * _parseClosures Iterates over an array and runs any anonymous functions it + * finds. Returns true if all of the closures it runs evaluate to true. $match + * is passed by refference and any closures found are removed from it before the + * method is complete. + * + * @static + * @access protected + * + * @param array $data dereferenced Array + * @param object $request The Lithium `Request` object + * @param array $roleOptions dereferenced Array + * @return boolean + */ + protected static function _parseClosures(array &$data, $request, array &$roleOptions = array()) { + $return = true; + foreach ($data as $key => $item) { + if (is_callable($item)) { + if ($return === true) { + $return = (boolean) $item($request, $roleOptions); + } + unset($data[$key]); + } + } + return $return; + } /** * @todo reduce Model Overhead (will duplicated in each model) * - * @param Request $request Object - * @return array|mixed $roles Roles with attachted User Models + * @param mixed $params The Lithium `Request` object, or an array with at least + * 'request', and 'params' + * @param array $options + * @return array|mixed $roles Roles with attached User Models */ - protected static function _getRolesByAuth($request, array $options = array()){ + protected static function _getRolesByAuth($params, array $options = array()) { $roles = array('*' => '*'); - foreach (array_keys(Auth::config()) as $key){ - if ($check = Auth::check($key, $request, $options)) { - $roles[$key] = $check; - } + foreach (array_keys(Auth::config()) as $key) { + if ($check = Auth::check($key, $params['request'], $options)) { + $roles[$key] = $check; + } } return $roles = array_filter($roles); } - /** - * _hasRole Compares the results from _getRolesByAuth with the array passed to it. - * - * @param mixed $requesters - * @param mixed $request - * @param array $options - * @access protected - * @return void - */ - protected function _hasRole($requesters, $request, array $options = array()) { - $authed = array_keys(static::_getRolesByAuth($request, $options)); - - $requesters = (array) $requesters; - if (in_array('*', $requesters)) { - return true; - } - - foreach ($requesters as $requester) { - if (in_array($requester, $authed)) { - return true; - } - } - return false; - } + /** + * _hasRole Compares the results from _getRolesByAuth with the array passed to it. + * + * @param mixed $requesters + * @param mixed $params + * @param array $options + * @access protected + * @return void + */ + protected function _accessable($resources, $roles, array $options = array()) { + $resources = (array) $resources; + if (in_array('*', $resources)) { + return true; + } + foreach ($resources as $resource) { + if (array_key_exists($resource, $roles)) { + return true; + } + } + return false; + } + + /** + * Itterates over an array and runs any anonymous functions it finds. Returns + * true if all of the closures it runs evaluate to true. $match is passed by + * reference and any closures found are removed from it before the method is complete. + * + * @param array $data + * @param mixed $request + * @access protected + * @return void + */ + protected function _run(&$data, $request = null, array &$options = array()) { + if (is_bool($data)) { + return $data; + } + + if (!is_array($data)) { + return false; + } + + $allow = true; + foreach ($data as $key => $item) { + if (is_callable($item)) { + if ($allow === true) { + $allow = (boolean) $item($request, $options); + } + unset($data[$key]); + } + } + return $allow; + } + + protected static function _hasRole($requesters, $params, array $options = array()) { + $authed = array_keys(static::_getRolesByAuth($params, $options)); + + $requesters = (array) $requesters; + if (in_array('*', $requesters)) { + return true; + } + + foreach ($requesters as $requester) { + if (in_array($requester, $authed)) { + return true; + } + } + return false; + } } ?> diff --git a/extensions/adapter/security/access/Rules.php b/extensions/adapter/security/access/Rules.php index b5900e9..4a64d96 100644 --- a/extensions/adapter/security/access/Rules.php +++ b/extensions/adapter/security/access/Rules.php @@ -4,114 +4,199 @@ use lithium\util\Set; +/** + * undocumented class + */ class Rules extends \lithium\core\Object { - // Rules are closures that must return true or false - protected static $_rules = array(); - - // Set some default rules to use - public static function __init() { - self::$_rules = array( - 'allowAll' => function() { - return true; - }, - 'denyAll' => function() { - return false; - }, - 'allowAnyUser' => function($user) { - return (!empty($user)) ? true:false; - }, - 'allowIp' => function($user, $request, $options) { - $options += array('ip' => false); - return $_SERVER['REMOTE_ADDR'] == $options['ip']; - } - ); - } - - /** - * The `Rules` adapter will use check to test the provided data - * against a number of given rules. Extra data that may be required - * to make an informed decision about access can be passed in the - * $options array. This extra data will vary from app to app and rules - * will need to be added to handle it. The default rules assume some - * general cases and more can be added or passed directly to this method. - * - * @param mixed $user The user data array that holds all necessary information about - * the user requesting access. Or false (because Auth::check() can return false). - * @param object $request The Lithium Request object. - * @param array $options An array of additional options. - * @return Array An empty array if access is allowed and an array with reasons for denial if denied. - */ - public function check($user, $request, array $options = array()) { - $options += array('rules' => array()); - if(empty($options['rules'])) { - return array('rule' => false, 'message' => $options['message'], 'redirect' => $options['redirect']); + /** + * Rules are named closures that must either return `true` or `false`. + * + * @var array + */ + protected $_rules = array(); + + /** + * Lists a subset of rules defined in `$_rules` which should be checked by default on every + * call to `check()` (unless overridden by passed options). + * + * @var array + */ + protected $_default = array(); + + /** + * Configuration that will be automatically assigned to class properties. + * + * @var array + */ + protected $_autoConfig = array('rules', 'default'); + + /** + * Sets default adapter configuration. + * + * @param array $config Adapter configuration, which includes the following default options: + * - `'rules'` _array_: An array of rules to be added to the default rules + * initialized by the adapter. See the `'rules'` option of the `check()` method + * for more information on the acceptable format of these values. + * - `'default'` _array_: The default list of rules to use when performing access + * checks. + * - `'allowAny'` _boolean_: If set to `true`, access checks will return successful + * if _any_ access rule passes. Otherwise, all are required to pass in order for + * the check to succeed. Defaults to `false`. + */ + public function __construct(array $config = array()) { + $defaults = array( + 'rules' => array(), + 'default' => array(), + 'allowAny' => false, + 'user' => function() {} + ); + parent::__construct($config + $defaults); + } + + /** + * Initializes default rules to use. + * + * @return void + */ + protected function _init() { + parent::_init(); + + $this->_rules += array( + 'allowAll' => function() { + return true; + }, + 'denyAll' => function() { + return false; + }, + 'allowAnyUser' => function($user) { + return $user ? true : false; + }, + 'allowIp' => function($user, $request, $options) { + $options += array('ip' => false); + + if (is_string($options['ip']) && strpos($options['ip'], '/') === 0) { + return (boolean) preg_match($options['ip'], $request->env('REMOTE_ADDR')); + } + if (is_array($options['ip'])) { + return in_array($request->env('REMOTE_ADDR'), $options['ip']); + } + return $request->env('REMOTE_ADDR') == $options['ip']; + } + ); } - - // If a single rule was passed, wrap it in an array so it can be iterated as if there were multiple - $rules = (isset($options['rules']['rule'])) ? array($options['rules']):$options['rules']; - - $access_response = array(); - - // Loop through all the rules. They must all pass. - foreach($rules as $rule) { - // make sure the rule is set and is a string to check for a closure to call or a closure itself - if((isset($rule['rule'])) && ((is_string($rule['rule'])) || (is_callable($rule['rule'])))) { - - $rule_result = false; - // The added rule closure will be passed the user data - if(in_array($rule['rule'], array_keys(self::$_rules))) { - // The rule closure will be passed the user, request and the rule array itself which could contain extra data required by the specific rule. - $rule_result = call_user_func(self::$_rules[$rule['rule']], $user, $request, $rule); - } elseif(is_callable($rule['rule'])) { - // The rule can be defined as a closure on the fly, no need to call add() - $rule_result = call_user_func($rule['rule'], $user, $request, $rule); + + /** + * The `Rules` adapter will use check to test the provided data + * against a number of given rules. Extra data that may be required + * to make an informed decision about access can be passed in the + * `$options` array. This extra data will vary from app to app and rules + * will need to be added to handle it. The default rules assume some + * general cases and more can be added or passed directly to this method. + * + * @param mixed $user The user data array that holds all necessary information about + * the user requesting access. Or false (because `Auth::check()` can return `false`). + * @param mixed $params The Lithium `Request` object, or an array with at least + * 'request', and 'params' + * @param array $options An array of additional options. + * @return array An empty array if access is allowed and an array with reasons for denial + * if denied. + */ + public function check($user, $params, array $options = array()) { + $defaults = array( + 'rules' => $this->_config['default'], + 'allowAny' => $this->_config['allowAny'] + ); + $options += $defaults; + $user = $user ?: $this->_config['user'](); + + if (!$options['rules']) { + $base = array('rule' => false, 'message' => null, 'redirect' => null); + return array_diff_key($options, $defaults) + $base; } - - if($rule_result === false) { - $access_response['rule'] = $rule['rule']; - $access_response['message'] = (isset($rule['message'])) ? $rule['message']:$options['message']; - $access_response['redirect'] = (isset($rule['redirect'])) ? $rule['redirect']:$options['redirect']; + + $rules = (isset($options['rules']['rule'])) ? array($options['rules']) : $options['rules']; + $result = array(); + + foreach ($rules as $rule) { + if (is_string($rule)) { + $rule = compact('rule'); + } + $ruleResult = $this->_call($rule, $user, $params['request'], $options); + + switch (true) { + case ($ruleResult === false && $options['allowAny']): + $result = $rule + array_diff_key($options, $defaults); + break; + case ($ruleResult === false): + return $rule + array_diff_key($options, $defaults); + case ($ruleResult !== false && $options['allowAny']): + return array(); + } } - - } - + return $result; } - - return $access_response; - } - - /** - * Adds an Access rule. This works much like the Validator class. - * All rules should be anonymous functions and will be passed - * $user, $request, and $options which will contain the entire - * rule array which contains its own name plus other data that - * could be used to determine access. - * - * @param string $name The rule name. - * @param function $rule The closure for the rule, which has to return true or false. - */ - public static function add($name, $rule = null) { - if (!is_array($name)) { - $name = array($name => $rule); - } - self::$_rules = Set::merge(self::$_rules, $name); - } - - /** - * Simply returns the rules that are currently available. - * Optionally, passing a name will return just that rule - * or false if it doesn't exist. - * - * @param string $name The rule name (optional). - * @return mixed Either an array of rule closures, a single rule closure, or false. - */ - public function getRules($name = false) { - if($name) { - return (isset(self::$_rules[$name])) ? self::$_rules[$name]:false; + + /** + * Extracts a callable rule either from a rule definition assigned as a closure, or a string + * reference to a rule defined in a key in the `$_rules` array. + * + * @param array $rule The rule definition array. + * @param mixed $user The value representing the user making the request. Usually an array. + * @param mixed $request The value representing request data or the object being access. + * @param array $options Any options passed to `check()`. + * @return boolean Returns `true` if the call to the rule was successful, otherwise `false` if + * the call failed, or if a callable rule was not found. + */ + protected function _call($rule, $user, $request, array $options) { + $callable = null; + + switch (true) { + case (is_callable($rule['rule'])): + $callable = $rule['rule']; + break; + case (in_array($rule['rule'], array_keys($this->_rules))): + $callable = $this->_rules[$rule['rule']]; + break; + } + return $callable ? call_user_func($callable, $user, $request, $rule + $options) : false; + } + + /** + * Adds an Access rule. This works much like the Validator class. + * All rules should be anonymous functions and will be passed + * $user, $request, and $options which will contain the entire + * rule array which contains its own name plus other data that + * could be used to determine access. + * + * @param string $name The rule name. + * @param function $rule The closure for the rule, which has to return true or false. + */ + public function add($name, $rule = null) { + $this->_rules = Set::merge($this->_rules, is_array($name) ? $name : array($name => $rule)); + } + + /** + * Simply returns the rules that are currently available. Optionally, passing a name will return + * just that rule or `false` if it doesn't exist. + * + * @param string $name The rule name (optional). + * @return mixed Either an array of rule closures, a single rule closure, or `false`. + */ + public function get($name = null) { + if ($name) { + return isset($this->_rules[$name]) ? $this->_rules[$name] : false; + } + return $this->_rules; + } + + /** + * @deprecated + * @param string $name The rule name (optional). + */ + public function getRules($name = null) { + return $this->get($name); } - return self::$_rules; - } - } -?> \ No newline at end of file + +?> diff --git a/extensions/adapter/security/access/Simple.php b/extensions/adapter/security/access/Simple.php index c1e14fd..f527002 100644 --- a/extensions/adapter/security/access/Simple.php +++ b/extensions/adapter/security/access/Simple.php @@ -2,9 +2,6 @@ namespace li3_access\extensions\adapter\security\access; -use lithium\core\Libraries; -use lithium\util\Set; - class Simple extends \lithium\core\Object { /** @@ -12,12 +9,14 @@ class Simple extends \lithium\core\Object { * It doesn't care about anything else. * * @param mixed $user The user data array that holds all necessary information about - * the user requesting access. Or false (because Auth::check() can return false). - * @param object $request The Lithium Request object. + * the user requesting access. Or `false` (because `Auth::check()` can return `false`). + * @param mixed $params The Lithium `Request` object, or an array with at least + * 'request', and 'params' * @param array $options An array of additional options. - * @return Array An empty array if access is allowed and an array with reasons for denial if denied. + * @return Array An empty array if access is allowed and an array with reasons for denial + * if denied. */ - public function check($user, $request, array $options = array()) { + public function check($user, $params, array $options = array()) { return !$user ? $options : array(); } } diff --git a/security/Access.php b/security/Access.php index d4ac4eb..5a7ff6a 100644 --- a/security/Access.php +++ b/security/Access.php @@ -5,7 +5,7 @@ * @author Tom Maiaroto * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org) * @license http://opensource.org/licenses/bsd-license.php The BSD License -*/ + */ namespace li3_access\security; @@ -23,45 +23,44 @@ */ class Access extends \lithium\core\Adaptable { - /** - * Stores configurations for various authentication adapters. - * - * @var object `Collection` of authentication configurations. - */ - protected static $_configurations = array(); - - /** - * Libraries::locate() compatible path to adapters for this class. - * - * @see lithium\core\Libraries::locate() - * @var string Dot-delimited path. - */ - protected static $_adapters = 'adapter.security.access'; + /** + * Stores configurations for various authentication adapters. + * + * @var object `Collection` of authentication configurations. + */ + protected static $_configurations = array(); + /** + * Libraries::locate() compatible path to adapters for this class. + * + * @see lithium\core\Libraries::locate() + * @var string Dot-delimited path. + */ + protected static $_adapters = 'adapter.security.access'; - /** - * Dynamic class dependencies. - * - * @var array Associative array of class names & their namespaces. - */ - protected static $_classes = array( - ); + /** + * Dynamic class dependencies. + * + * @var array Associative array of class names & their namespaces. + */ + protected static $_classes = array( + ); - /** - * Called when an adapter configuration is first accessed, this method sets the default - * configuration for session handling. While each configuration can use its own session class - * and options, this method initializes them to the default dependencies written into the class. - * For the session key name, the default value is set to the name of the configuration. - * - * @param string $name The name of the adapter configuration being accessed. - * @param array $config The user-specified configuration. - * @return array Returns an array that merges the user-specified configuration with the - * generated default values. - */ - protected static function _initConfig($name, $config) { - $defaults = array(); - $config = parent::_initConfig($name, $config) + $defaults; - return $config; - } + /** + * Called when an adapter configuration is first accessed, this method sets the default + * configuration for session handling. While each configuration can use its own session class + * and options, this method initializes them to the default dependencies written into the class. + * For the session key name, the default value is set to the name of the configuration. + * + * @param string $name The name of the adapter configuration being accessed. + * @param array $config The user-specified configuration. + * @return array Returns an array that merges the user-specified configuration with the + * generated default values. + */ + protected static function _initConfig($name, $config) { + $defaults = array(); + $config = parent::_initConfig($name, $config) + $defaults; + return $config; + } /** * Performs an access check against the specified configuration, and returns true @@ -73,29 +72,38 @@ protected static function _initConfig($name, $config) { * perhaps, login. * * @param string $name The name of the `Access` configuration/adapter to check against. - * @param mixed $user The user data that holds all necessary information about - * the user requesting access. Or `false` (because Auth::check() can return `false`). - * @param object $request The Lithium Request object. + * @param mixed $user The user data array that holds all necessary information about + * the user requesting access. Or `false` (because `Auth::check()` can return `false`). + * @param mixed $params The Lithium `Request` object, or an array with at least + * 'request', and 'params' * @param array $options An array of additional options. - * @return Array An empty array if access is allowed and an array with reasons for denial if denied. + * @return Array An empty array if access is allowed and an array with reasons for denial + * if denied. */ - public static function check($name, $user, $request, array $options = array()) { + public static function check($name, $user, $params, array $options = array()) { $defaults = array( - 'message' => 'You are not permitted to access this area.', 'redirect' => '/' + 'message' => 'You are not authorized to access this page.', + 'redirect' => 'Users::login' ); $options += $defaults; if (($config = static::_config($name)) === null) { throw new ConfigException("Configuration `{$name}` has not been defined."); } + if (!is_array($params)) { + $params = array( + 'request' => $params, + 'params' => isset($params->params) ? $params->params : array() + ); + } $filter = function($self, $params) use ($name) { - return $self::adapter($name)->check($params['user'], $params['request'], $params['options']); + return $self::adapter($name)->check( + $params['user'], $params['params'], $params['options'] + ); }; - $filters = (array) $config['filters']; - $params = compact('user', 'request', 'options'); - return static::_filter(__FUNCTION__, $params, $filter, $filters); + $params = compact('user', 'params', 'options'); + return static::_filter(__FUNCTION__, $params, $filter, (array) $config['filters']); } - } ?> diff --git a/security/AccessDeniedException.php b/security/AccessDeniedException.php index 9497918..7b05d91 100644 --- a/security/AccessDeniedException.php +++ b/security/AccessDeniedException.php @@ -9,8 +9,7 @@ namespace li3_access\security; /** - * A `NetworkException` may be thrown whenever an unsuccessful attempt is made to connect to a - * remote service over the network. This may be a web service, a database, or another network + * An `AccessDeniedException` is thrown whenever an unhandled attempt is made to access a restricted * resource. */ class AccessDeniedException extends \RuntimeException { diff --git a/tests/cases/extensions/adapter/security/access/AuthRbacTest.php b/tests/cases/extensions/adapter/security/access/AuthRbacTest.php index 0570819..56aa0fc 100644 --- a/tests/cases/extensions/adapter/security/access/AuthRbacTest.php +++ b/tests/cases/extensions/adapter/security/access/AuthRbacTest.php @@ -16,213 +16,326 @@ class AuthRbacTest extends \lithium\test\Unit { - public function setUp() { - Auth::config(array( - 'user' => array( - 'adapter' => '\li3_access\tests\mocks\extensions\adapter\auth\MockAuthAdapter' - ) - )); - - Access::config(array( - 'test_no_roles_configured' => array( - 'adapter' => 'AuthRbac' - ), - 'test_check' => array( - 'adapter' => 'AuthRbac', - 'roles' => array( - 'allow' => array( - 'requesters' => 'user', - 'match' => '*::*' - ) - ) - ), - 'test_closures' => array( - 'adapter' => 'AuthRbac', - 'roles' => array( - array( - 'requesters' => '*', - 'allow' => array(function($request, &$roleOptions) { - $roleOptions['message'] = 'Test allow options set.'; - return $request->params['allow'] ? true : false; - }), - 'match' => array( - function($request) { - return $request->params['match'] ? true : false; - }, - 'controller' => 'TestControllers', - 'action' => 'test_action' - ) - ) - ) - ), - 'test_message_override' => array( - 'adapter' => 'AuthRbac', - 'roles' => array( - array( - 'allow' => false, - 'requesters' => '*', - 'match' => '*::*' - ), - array( - 'message' => 'Rule access denied message.', - 'redirect' => '/', - 'requesters' => 'user', - 'match' => 'TestControllers::test_action' - ), - array( - 'message' => 'Test no overwrite.', - 'redirect' => '/test_no_overwrite', - 'requesters' => 'user', - 'match' => null - ) - ) - ) - )); - } - - public function tearDown() { - Auth::clear('user'); - } - - public function testCheck() { - $request = new Request(array('params' => array('library' => 'test_library', 'controller' => 'test_controllers', 'action' => 'test_action'))); - - $guest = array(); - $user = array('username' => 'test'); - - $request->data = $guest; - $expected = array('message' => 'You are not permitted to access this area.', 'redirect' => '/'); - $result = Access::check('test_check', $guest, $request, array('checkSession' => false)); - $this->assertIdentical($expected, $result); - - $request->data = $user; - $expected = array(); - $result = Access::check('test_check', $user, $request, array('checkSession' => false, 'success' => true)); - $this->assertIdentical($expected, $result); - } - - public function testCheckMessageOverride() { - $request = new Request(array('params' => array('library' => 'test_library', 'controller' => 'test_controllers', 'action' => 'test_action'))); - - $guest = array(); - $user = array('username' => 'test'); - - $request->data = $guest; - $expected = array('message' => 'Rule access denied message.', 'redirect' => '/'); - $result = Access::check('test_message_override', $guest, $request, array('checkSession' => false)); - $this->assertIdentical($expected, $result); - - $request->data = $user; - $expected = array(); - $result = Access::check('test_message_override', $user, $request, array('checkSession' => false, 'success' => 'true')); - $this->assertIdentical($expected, $result); - - $request->params = array('controller' => 'test_controllers', 'action' => 'test_deinied_action'); - - $request->data = $guest; - $expected = array('message' => 'You are not permitted to access this area.', 'redirect' => '/'); - $result = Access::check('test_message_override', $guest, $request, array('checkSession' => false)); - $this->assertIdentical($expected, $result); - - $request->data = $user; - $expected = array('message' => 'You are not permitted to access this area.', 'redirect' => '/'); - $result = Access::check('test_message_override', $user, $request, array('checkSession' => false)); - $this->assertIdentical($expected, $result); - - $request->data = $user; - $expected = array('message' => 'Message override!', 'redirect' => '/new_redirect'); - $result = Access::check('test_message_override', $user, $request, array('checkSession' => false, 'message' => 'Message override!', 'redirect' => '/new_redirect')); - $this->assertIdentical($expected, $result); - } - - public function testParseMatch() { - $request = new Request(array('params' => array('library' => 'test_library', 'controller' => 'test_controllers', 'action' => 'test_action'))); - - $match = array('library' => 'test_library', 'controller' => 'TestControllers', 'action' => 'test_action'); - $this->assertTrue(Access::adapter('test_check')->parseMatch($match, $request)); - - $match = array('controller' => 'TestControllers', 'action' => 'test_action'); - $this->assertTrue(Access::adapter('test_check')->parseMatch($match, $request)); - - $match = array('library' => 'test_library', 'action' => 'test_action'); - $this->assertTrue(Access::adapter('test_check')->parseMatch($match, $request)); - - $match = array('library' => 'test_library', 'controller' => 'TestControllers'); - $this->assertTrue(Access::adapter('test_check')->parseMatch($match, $request)); - - $match = array('library' => 'test_no_match', 'controller' => 'TestControllers', 'action' => 'test_action'); - $this->assertFalse(Access::adapter('test_check')->parseMatch($match, $request)); - - $match = 'TestControllers::test_action'; - $this->assertTrue(Access::adapter('test_check')->parseMatch($match, $request)); - - $match = 'TestControllers::*'; - $this->assertTrue(Access::adapter('test_check')->parseMatch($match, $request)); - - $match = '*::test_action'; - $this->assertTrue(Access::adapter('test_check')->parseMatch($match, $request)); - - $match = '*::*'; - $this->assertTrue(Access::adapter('test_check')->parseMatch($match, $request)); - - $match = array('library' => 'test_library', '*::*'); - $this->assertTrue(Access::adapter('test_check')->parseMatch($match, $request)); - - $match = array('library' => 'test_no_match', '*::*'); - $this->assertFalse(Access::adapter('test_check')->parseMatch($match, $request)); - - $match = null; - $this->assertFalse(Access::adapter('test_check')->parseMatch($match, $request)); - - $match = true; - $test = function() { return true; }; - $this->assertTrue(Access::adapter('test_closures')->parseMatch(array($test), $request)); - - $match = false; - $test = function() { return false; }; - $this->assertFalse(Access::adapter('test_closures')->parseMatch(array($test), $request)); - - $match = false; - $this->assertFalse(Access::adapter('test_closures')->parseMatch(array(), $request)); - } - - public function testClosures() { - $request = new Request(array('params' => array('controller' => 'test_controllers', 'action' => 'test_action'))); - - $user = $request->data = array('username' => 'test'); - $authSuccess = array('checkSession' => false, 'success' => true); - - $request->params['match'] = true; - $request->params['allow'] = true; - $expected = array(); - $result = Access::check('test_closures', $user, $request, $authSuccess); - $this->assertIdentical($expected, $result); - - $request->params['match'] = true; - $request->params['allow'] = false; - $expected = array('message' => 'Test allow options set.', 'redirect' => '/'); - $result = Access::check('test_closures', $user, $request, $authSuccess); - $this->assertIdentical($expected, $result); - - $request->params = array('controller' => 'TestControllers', 'action' => 'bad_action'); - - $request->params['match'] = true; - $request->params['allow'] = true; - $result = Access::check('test_closures', $user, $request, $authSuccess); - $expected = array('message' => 'You are not permitted to access this area.', 'redirect' => '/'); - $this->assertIdentical($expected, $result); - } - - public function testNoRolesConfigured() { - $request = new Request(); - - $config = Access::config('test_no_roles_configured'); - $request->params = array('controller' => 'Tests', 'action' => 'granted'); - - $this->assertTrue(empty($config['roles'])); - $this->expectException('No roles defined for adapter configuration.'); - Access::check('test_no_roles_configured', array('guest' => null), $request); - } - + public function setUp() { + Auth::config(array( + 'user' => array( + 'adapter' => 'li3_access\tests\mocks\extensions\adapter\auth\MockAuthAdapter' + ) + )); + + Access::config(array( + 'test_no_roles_configured' => array('adapter' => 'AuthRbac'), + 'test_check' => array( + 'adapter' => 'AuthRbac', + 'roles' => array( + 'allow' => array( + 'requesters' => 'user', + 'match' => '*::*' + ) + ) + ), + 'test_closures' => array( + 'adapter' => 'AuthRbac', + 'roles' => array( + array( + 'requesters' => '*', + 'allow' => array(function($request, &$roleOptions) { + $roleOptions['message'] = 'Test allow options set.'; + return $request->params['allow'] ? true : false; + }), + 'match' => array( + function($request) { + return $request->params['match'] ? true : false; + }, + 'controller' => 'TestControllers', + 'action' => 'test_action' + ) + ) + ) + ), + 'test_allow_closure' => array( + 'adapter' => 'AuthRbac', + 'roles' => array( + array( + 'requesters' => '*', + 'match' => '*::*', + 'allow' => function($request, &$roleOptions) { + $roleOptions['message'] = 'Test allow options set.'; + return $request->params['allow'] ? true : false; + } + ) + ) + ), + 'test_allow_closure_match' => array( + 'adapter' => 'AuthRbac', + 'roles' => array( + array( + 'requesters' => '*', + 'match' => function($request) { + return !empty($request->params['allow_match']); + }, + 'allow' => function($request, &$roleOptions) { + $roleOptions['message'] = 'Test allow options set 2.'; + return $request->params['allow'] ? true : false; + } + ) + ) + ), + 'test_message_override' => array( + 'adapter' => 'AuthRbac', + 'roles' => array( + array( + 'allow' => false, + 'requesters' => '*', + 'match' => '*::*' + ), + array( + 'message' => 'Rule access denied message.', + 'redirect' => '/', + 'requesters' => 'user', + 'match' => 'TestControllers::test_action' + ), + array( + 'message' => 'Test no overwrite.', + 'redirect' => '/test_no_overwrite', + 'requesters' => 'user', + 'match' => null + ) + ) + ) + )); + } + + public function tearDown() { + Auth::clear('user'); + } + + public function testCheck() { + $request = new Request(array('params' => array( + 'library' => 'test_library', + 'controller' => 'test_controllers', + 'action' => 'test_action' + ))); + + $guest = array(); + $user = array('username' => 'test'); + + $request->data = $guest; + $expected = array( + 'message' => 'You are not permitted to access this area.', + 'redirect' => '/' + ); + $result = Access::check('test_check', $guest, $request, array('checkSession' => false)); + $this->assertIdentical($expected, $result); + + $request->data = $user; + $expected = array(); + $result = Access::check('test_check', $user, $request, array( + 'checkSession' => false, + 'success' => true + )); + $this->assertIdentical($expected, $result); + } + + public function testCheckMessageOverride() { + $request = new Request(array('params' => array( + 'library' => 'test_library', + 'controller' => 'test_controllers', + 'action' => 'test_action' + ))); + + $guest = array(); + $user = array('username' => 'test'); + + $request->data = $guest; + $expected = array('message' => 'Rule access denied message.', 'redirect' => '/'); + $result = Access::check('test_message_override', $guest, $request, array( + 'checkSession' => false + )); + $this->assertIdentical($expected, $result); + + $request->data = $user; + $expected = array(); + $result = Access::check('test_message_override', $user, $request, array( + 'checkSession' => false, + 'success' => 'true' + )); + $this->assertIdentical($expected, $result); + + $request->params = array( + 'controller' => 'test_controllers', + 'action' => 'test_deinied_action' + ); + + $request->data = $guest; + $expected = array( + 'message' => 'You are not permitted to access this area.', + 'redirect' => '/' + ); + $result = Access::check('test_message_override', $guest, $request, array( + 'checkSession' => false + )); + $this->assertIdentical($expected, $result); + + $request->data = $user; + $expected = array( + 'message' => 'You are not permitted to access this area.', + 'redirect' => '/' + ); + $result = Access::check('test_message_override', $user, $request, array( + 'checkSession' => false + )); + $this->assertIdentical($expected, $result); + + $request->data = $user; + $expected = array('message' => 'Message override!', 'redirect' => '/new_redirect'); + $result = Access::check('test_message_override', $user, $request, array( + 'checkSession' => false, + 'message' => 'Message override!', + 'redirect' => '/new_redirect' + )); + $this->assertIdentical($expected, $result); + } + + public function testParseMatch() { + $params = array( + 'library' => 'test_library', + 'controller' => 'test_controllers', + 'action' => 'test_action' + ); + $request = new Request(array('params' => $params)); + + $match = array( + 'library' => 'test_library', + 'controller' => 'TestControllers', + 'action' => 'test_action' + ); + $this->assertTrue(Access::adapter('test_check')->parseMatch($match, compact('request', 'params'))); + + $match = array('controller' => 'TestControllers', 'action' => 'test_action'); + $this->assertTrue(Access::adapter('test_check')->parseMatch($match, compact('request', 'params'))); + + $match = array('library' => 'test_library', 'action' => 'test_action'); + $this->assertTrue(Access::adapter('test_check')->parseMatch($match, compact('request', 'params'))); + + $match = array('library' => 'test_library', 'controller' => 'TestControllers'); + $this->assertTrue(Access::adapter('test_check')->parseMatch($match, compact('request', 'params'))); + + $match = array( + 'library' => 'test_no_match', + 'controller' => 'TestControllers', + 'action' => 'test_action' + ); + $this->assertFalse(Access::adapter('test_check')->parseMatch($match, compact('request', 'params'))); + + $match = 'TestControllers::test_action'; + $this->assertTrue(Access::adapter('test_check')->parseMatch($match, compact('request', 'params'))); + + $match = 'TestControllers::*'; + $this->assertTrue(Access::adapter('test_check')->parseMatch($match, compact('request', 'params'))); + + $match = '*::test_action'; + $this->assertTrue(Access::adapter('test_check')->parseMatch($match, compact('request', 'params'))); + + $match = '*::*'; + $this->assertTrue(Access::adapter('test_check')->parseMatch($match, compact('request', 'params'))); + + $match = array('library' => 'test_library', '*::*'); + $this->assertTrue(Access::adapter('test_check')->parseMatch($match, compact('request', 'params'))); + + $match = array('library' => 'test_no_match', '*::*'); + $this->assertFalse(Access::adapter('test_check')->parseMatch($match, compact('request', 'params'))); + + $match = null; + $this->assertFalse(Access::adapter('test_check')->parseMatch($match, compact('request', 'params'))); + + $test = function() { return true; }; + $this->assertTrue(Access::adapter('test_closures')->parseMatch(array($test), compact('request', 'params'))); + + $test = function() { return false; }; + $this->assertFalse(Access::adapter('test_closures')->parseMatch(array($test), compact('request', 'params'))); + $this->assertFalse(Access::adapter('test_closures')->parseMatch(array(), compact('request', 'params'))); + + $params = array( + 'controller' => 'lithium\test\Controller', + 'action' => 'index' + ); + $request = new Request(array('params' => $params)); + $match = 'Controller::*'; + $this->assertFalse(Access::adapter('test_check')->parseMatch($match, compact('request', 'params'))); + $match = 'lithium\test\Controller::*'; + $this->assertTrue(Access::adapter('test_check')->parseMatch($match, compact('request', 'params'))); + } + + public function testClosures() { + $request = new Request(array('params' => array( + 'controller' => 'test_controllers', 'action' => 'test_action' + ))); + + $user = $request->data = array('username' => 'test'); + $authSuccess = array('checkSession' => false, 'success' => true); + + $request->params['match'] = true; + $request->params['allow'] = true; + $result = Access::check('test_closures', $user, $request, $authSuccess); + $this->assertIdentical(array(), $result); + + $request->params['match'] = true; + $request->params['allow'] = false; + $expected = array('message' => 'Test allow options set.', 'redirect' => '/'); + $result = Access::check('test_closures', $user, $request, $authSuccess); + $this->assertIdentical($expected, $result); + + $request->params = array('controller' => 'TestControllers', 'action' => 'bad_action'); + + $request->params['match'] = true; + $request->params['allow'] = true; + $result = Access::check('test_closures', $user, $request, $authSuccess); + $expected = array( + 'message' => 'You are not permitted to access this area.', + 'redirect' => '/' + ); + $this->assertIdentical($expected, $result); + + $request->params['allow'] = true; + $result = Access::check('test_allow_closure', $user, $request, $authSuccess); + $expected = array(); + $this->assertIdentical($expected, $result); + + $request->params['allow'] = false; + $result = Access::check('test_allow_closure', $user, $request, $authSuccess); + $expected = array('message' => 'Test allow options set.', 'redirect' => '/'); + $this->assertIdentical($expected, $result); + + $request->params['allow'] = true; + $request->params['allow_match'] = true; + $result = Access::check('test_allow_closure_match', $user, $request, $authSuccess); + $expected = array(); + $this->assertIdentical($expected, $result); + + $request->params['allow'] = false; + $request->params['allow_match'] = true; + $result = Access::check('test_allow_closure_match', $user, $request, $authSuccess); + $expected = array('message' => 'Test allow options set 2.', 'redirect' => '/'); + $this->assertIdentical($expected, $result); + + $request->params['allow'] = true; + $request->params['allow_match'] = false; + $result = Access::check('test_allow_closure_match', $user, $request, $authSuccess); + $expected = array('message' => 'You are not permitted to access this area.', 'redirect' => '/'); + $this->assertIdentical($expected, $result); + } + + public function testNoRolesConfigured() { + $request = new Request(); + + $config = Access::config('test_no_roles_configured'); + $request->params = array('controller' => 'Tests', 'action' => 'granted'); + + $this->assertTrue(empty($config['roles'])); + $this->expectException('No roles defined for adapter configuration.'); + Access::check('test_no_roles_configured', array('guest' => null), $request); + } } -?> + +?> \ No newline at end of file diff --git a/tests/cases/extensions/adapter/security/access/RulesTest.php b/tests/cases/extensions/adapter/security/access/RulesTest.php index 3b645b0..0bcbf3c 100644 --- a/tests/cases/extensions/adapter/security/access/RulesTest.php +++ b/tests/cases/extensions/adapter/security/access/RulesTest.php @@ -9,85 +9,219 @@ namespace li3_access\tests\cases\extensions\adapter\security\access; -use lithium\net\http\Request; -use li3_access\security\Access; +use lithium\action\Request; +use li3_access\extensions\adapter\security\access\Rules; class RulesTest extends \lithium\test\Unit { - public function setUp() { - Access::config(array( - 'test_rulebased' => array( - 'adapter' => 'Rules' - ) - )); - } - - public function tearDown() {} - - public function testCheck() { - $request = new Request(); - - // Multiple rules, they should all pass - $rules = array( - array('rule' => 'allowAnyUser', 'message' => 'You must be logged in.'), - array('rule' => 'allowAll', 'message' => 'You must be logged in.'), - array('rule' => 'allowIp', 'message' => 'You can not access this from your location. (IP: ' . $_SERVER['REMOTE_ADDR'] . ')', 'ip' => $_SERVER['REMOTE_ADDR']) - ); - $expected = array(); - $result = Access::check('test_rulebased', array('username' => 'Tom'), $request, array('rules' => $rules)); - $this->assertEqual($expected, $result); - - // Single rule in multi-demnsional array - $rules = array( - array('rule' => 'denyAll', 'message' => 'You must be logged in.') - ); - $expected = array('rule' => 'denyAll', 'message' => 'You must be logged in.', 'redirect' => '/'); - $result = Access::check('test_rulebased', array('username' => 'Tom'), $request, array('rules' => $rules)); - $this->assertEqual($expected, $result); - - // Single rule (single array), but it should fail because user is an empty array - $rules = array('rule' => 'allowAnyUser', 'message' => 'You must be logged in.'); - $expected = array('rule' => 'allowAnyUser', 'message' => 'You must be logged in.', 'redirect' => '/'); - $result = Access::check('test_rulebased', array(), $request, array('rules' => $rules)); - $this->assertEqual($expected, $result); - // and if false instead of an empty array (because one might typically run Auth:check() which could return false) - $result = Access::check('test_rulebased', false, $request, array('rules' => $rules)); - $this->assertEqual($expected, $result); - - // No rules - $expected = array('rule' => false, 'message' => 'You are not permitted to access this area.', 'redirect' => '/'); - $result = Access::check('test_rulebased', array('username' => 'Tom'), $request); - $this->assertEqual($expected, $result); - - // Adding a rule "on the fly" by passing a closure, this rule should pass - $rules = array( - array('rule' => function($user, $request, $options) { return $user['username'] == 'Tom'; }, 'message' => 'Access denied.') - ); - $expected = array(); - $result = Access::check('test_rulebased', array('username' => 'Tom'), $request, array('rules' => $rules)); - $this->assertEqual($expected, $result); - } - - public function testAdd() { - $request = new Request(); - - // The add() method to add a rule - Access::adapter('test_rulebased')->add('testDeny', function($user, $request, $options) { - return false; - }); - - $rules = array( - array('rule' => 'testDeny', 'message' => 'Access denied.') - ); - $expected = array('rule' => 'testDeny', 'message' => 'Access denied.', 'redirect' => '/'); - $result = Access::check('test_rulebased', array('username' => 'Tom'), $request, array('rules' => $rules)); - $this->assertEqual($expected, $result); - - // Make sure the rule got added to the $_rules property - $this->assertTrue(is_callable(Access::adapter('test_rulebased')->getRules('testDeny'))); - - $this->assertTrue(is_array(Access::adapter('test_rulebased')->getRules())); - } - + public function testPatternBasedIpMatching() { + $request = new Request(array('env' => array('REMOTE_ADDR' => '10.0.1.2'))); + $adapter = new Rules(); + + $rules = array(array( + 'rule' => 'allowIp', + 'message' => 'You can not access this from your location.', + 'ip' => '/10\.0\.1\.\d+/' + )); + $result = $adapter->check(array(), compact('request'), compact('rules')); + $this->assertEqual(array(), $result); + + $request = new Request(array('env' => array('REMOTE_ADDR' => '10.0.1.255'))); + $result = $adapter->check(array(), compact('request'), compact('rules')); + $this->assertEqual(array(), $result); + + $request = new Request(array('env' => array('REMOTE_ADDR' => '10.0.2.1'))); + $result = $adapter->check(array(), compact('request'), compact('rules')); + $this->assertEqual('You can not access this from your location.', $result['message']); + } + + public function testArrayBasedIpMatching() { + $adapter = new Rules(); + $rules = array(array( + 'rule' => 'allowIp', + 'message' => 'You can not access this from your location.', + 'ip' => array('10.0.1.2', '10.0.1.3', '10.0.1.4') + )); + + foreach (array(2, 3, 4) as $i) { + $request = new Request(array('env' => array('REMOTE_ADDR' => "10.0.1.{$i}"))); + $result = $adapter->check(array(), compact('request'), compact('rules')); + $this->assertEqual(array(), $result); + } + + foreach (array(1, 5, 255) as $i) { + $request = new Request(array('env' => array('REMOTE_ADDR' => "10.0.1.{$i}"))); + $result = $adapter->check(array(), compact('request'), compact('rules')); + $this->assertEqual('You can not access this from your location.', $result['message']); + } + } + + public function testCheck() { + $user = array('username' => 'Tom'); + $request = new Request(array('env' => array('REMOTE_ADDR' => '10.0.1.1'))); + $adapter = new Rules(); + + $rules = array( + array('rule' => 'allowAnyUser', 'message' => 'You must be logged in.'), + array('rule' => 'allowAll', 'message' => 'You must be logged in.'), + array( + 'rule' => 'allowIp', + 'message' => 'You can not access this from your location. (IP: 10.0.1.1)', + 'ip' => '10.0.1.1' + ) + ); + $result = $adapter->check($user, compact('request'), compact('rules')); + $this->assertEqual(array(), $result); + + $rules = array(array('rule' => 'denyAll', 'message' => 'You must be logged in.')); + $expected = array('rule' => 'denyAll', 'message' => 'You must be logged in.'); + $result = $adapter->check($user, compact('request'), compact('rules')); + $this->assertEqual($expected, $result); + + $rules = array('rule' => 'allowAnyUser', 'message' => 'You must be logged in.'); + $expected = array('rule' => 'allowAnyUser', 'message' => 'You must be logged in.'); + $result = $adapter->check(array(), compact('request'), compact('rules')); + $this->assertEqual($expected, $result); + + $result = $adapter->check(false, compact('request'), compact('rules')); + $this->assertEqual($expected, $result); + } + + /** + * Test access checking with no passed or defined rules. + */ + public function testCheckNoRules() { + $user = array('username' => 'Tom'); + $request = new Request(); + $adapter = new Rules(); + + $expected = array('rule' => false, 'message' => null, 'redirect' => null); + $result = $adapter->check($user, compact('request')); + $this->assertEqual($expected, $result); + } + + /** + * Tests checking against a list of rules that are passed on the fly. + */ + public function testPassingRules() { + $user = array('username' => 'Tom'); + $request = new Request(); + $adapter = new Rules(); + + $rules = array( + array('message' => 'Access denied.', 'rule' => function($user, $request, $options) { + return $user['username'] == 'Tom'; + }) + ); + $expected = array(); + $result = $adapter->check($user, compact('request'), compact('rules')); + $this->assertEqual($expected, $result); + } + + public function testAdd() { + $request = new Request(); + $user = array('username' => 'Tom'); + $adapter = new Rules(); + + $adapter->add('testDeny', function($user, $request, $options) { + return false; + }); + + $rules = array(array('rule' => 'testDeny', 'message' => 'Access denied.')); + $expected = array('rule' => 'testDeny', 'message' => 'Access denied.'); + $result = $adapter->check($user, compact('request'), compact('rules')); + $this->assertEqual($expected, $result); + + $this->assertTrue(is_callable($adapter->get('testDeny'))); + $this->assertEqual($adapter->get('testDeny'), $adapter->getRules('testDeny')); + + $rules = $adapter->get(); + $this->assertTrue(is_array($rules)); + $this->assertTrue(in_array('testDeny', array_keys($rules))); + } + + /** + * Tests that calls only fail when all rules fail if `'allowAny'` is set. + */ + public function testAllowAnyRule() { + $request = new Request(); + $adapter = new Rules(); + $user = array('username' => 'Tom'); + + $rules = array( + array('rule' => 'allowAll', 'message' => 'Access denied.'), + array('rule' => 'denyAll', 'message' => 'Access denied.') + ); + $result = $adapter->check($user, compact('request'), compact('rules')); + $this->assertEqual(array('rule' => 'denyAll', 'message' => 'Access denied.'), $result); + + $adapter = new Rules(array('allowAny' => true)); + $result = $adapter->check($user, compact('request'), compact('rules')); + $this->assertEqual(array(), $result); + + $result = $adapter->check($user, compact('request'), array('rules' => array('denyAll', 'allowAll'))); + $this->assertEqual(array(), $result); + } + + /** + * Tests that checks against invalid rules return an invalid rule array. + */ + public function testInvalidRule() { + $request = new Request(); + $adapter = new Rules(); + $user = array('username' => 'Tom'); + + $result = $adapter->check($user, compact('request'), array('rules' => array('badness'))); + $this->assertEqual(array('rule' => 'badness'), $result); + } + + /** + * Tests that options passed to `Rules::check()` are passed to each rule doing the checking. + */ + public function testOptionsPassedToRule() { + $request = new Request(); + $user = array('username' => 'Tom'); + $adapter = new Rules(array( + 'rules' => array( + 'foobar' => function($user, $request, $options) { + return $options['foo'] == 'bar'; + } + ), + 'default' => array('foobar') + )); + + $result = $adapter->check($user, compact('request'), array('foo' => 'baz')); + $this->assertEqual(array('rule' => 'foobar', 'foo' => 'baz'), $result); + $result = $adapter->check($user, compact('request'), array('foo' => 'bar')); + $this->assertEqual(array(), $result); + } + + /** + * Tests that user information is automatically retrieved via the closure in the `'user'` + * config. + */ + public function testAutoUser() { + $request = new Request(); + $user = array('username' => 'Tom'); + $adapter = new Rules(array( + 'rules' => array( + 'user' => function($user, $request, $options) { + return isset($user['username']) && $user['username'] == 'Tom'; + } + ), + 'default' => array('user'), + 'user' => function() use ($user) { return $user; } + )); + + $result = $adapter->check($user, compact('request')); + $this->assertEqual(array(), $result); + + $result = $adapter->check(null, compact('request')); + $this->assertEqual(array(), $result); + + $result = $adapter->check(array('username' => 'Bob'), compact('request')); + $this->assertEqual(array('rule' => 'user'), $result); + } } -?> + +?> \ No newline at end of file diff --git a/tests/cases/extensions/adapter/security/access/SimpleTest.php b/tests/cases/extensions/adapter/security/access/SimpleTest.php index d045f0b..42ea441 100644 --- a/tests/cases/extensions/adapter/security/access/SimpleTest.php +++ b/tests/cases/extensions/adapter/security/access/SimpleTest.php @@ -5,37 +5,36 @@ * @author Tom Maiaroto * @copyright Copyright 2010, Union of RAD (http://union-of-rad.org) * @license http://opensource.org/licenses/bsd-license.php The BSD License -*/ + */ namespace li3_access\tests\cases\extensions\adapter\security\access; -use \li3_access\security\Access; -use \lithium\net\http\Request; +use li3_access\security\Access; +use lithium\net\http\Request; class SimpleTest extends \lithium\test\Unit { - public function setUp() { - Access::config(array( - 'test_access' => array( - 'adapter' => 'Simple' - ) - )); - } + public function setUp() { + Access::config(array( + 'test_access' => array('adapter' => 'Simple') + )); + } - public function tearDown() {} + public function tearDown() {} - public function testCheck() { - $request = new Request(); + public function testCheck() { + $request = new Request(); - $expected = array(); - $result = Access::check('test_access', array('username' => 'Tom'), $request); - $this->assertEqual($expected, $result); - - $expected = array('message' => 'Access denied.', 'redirect' => '/login'); - $result = Access::check('test_access', false, $request, array('redirect' => '/login', 'message' => 'Access denied.')); - $this->assertEqual($expected, $result); - } + $result = Access::check('test_access', array('username' => 'Tom'), $request); + $this->assertEqual(array(), $result); + $expected = array('message' => 'Access denied.', 'redirect' => '/login'); + $result = Access::check('test_access', false, $request, array( + 'redirect' => '/login', + 'message' => 'Access denied.' + )); + $this->assertEqual($expected, $result); + } } -?> +?> \ No newline at end of file diff --git a/tests/cases/security/AccessTest.php b/tests/cases/security/AccessTest.php index b433b37..4734118 100644 --- a/tests/cases/security/AccessTest.php +++ b/tests/cases/security/AccessTest.php @@ -9,63 +9,67 @@ namespace li3_access\tests\cases\security; -use \li3_access\security\Access; -use \lithium\net\http\Request; +use li3_access\security\Access; +use lithium\net\http\Request; class AccessTest extends \lithium\test\Unit { - public function setUp() { - Access::config(array( - 'test_access' => array( - 'adapter' => 'Simple' - ), - 'test_access_with_filters' => array( - 'adapter' => 'Simple', - 'filters' => array( - function($self, $params, $chain) { - return $chain->next($self, $params, $chain); - }, - function($self, $params, $chain) { - if (!$params['user']) { - return array('message' => 'Access denied.', 'redirect' => $params['options']['redirect']); - } else { - return $chain->next($self, $params, $chain); - } - } - ) - ) - )); - } + public function setUp() { + Access::config(array( + 'test_access' => array('adapter' => 'Simple'), + 'test_access_with_filters' => array( + 'adapter' => 'Simple', + 'filters' => array( + function($self, $params, $chain) { + return $chain->next($self, $params, $chain); + }, + function($self, $params, $chain) { + if (!$params['user']) { + return array( + 'message' => 'Access denied.', + 'redirect' => $params['options']['redirect'] + ); + } + return $chain->next($self, $params, $chain); + } + ) + ) + )); + } - public function tearDown() {} + public function tearDown() {} - public function testCheck() { - $request = new Request(); + public function testCheck() { + $request = new Request(); - $expected = array(); - $result = Access::check('test_access', array('username' => 'Tom'), $request); - $this->assertEqual($expected, $result); + $expected = array(); + $result = Access::check('test_access', array('username' => 'Tom'), $request); + $this->assertEqual($expected, $result); - $expected = array('message' => 'Access denied.', 'redirect' => '/login'); - $result = Access::check('test_access', false, $request, array('redirect' => '/login', 'message' => 'Access denied.')); - $this->assertEqual($expected, $result); - } + $expected = array('message' => 'Access denied.', 'redirect' => '/login'); + $result = Access::check('test_access', false, $request, array( + 'redirect' => '/login', + 'message' => 'Access denied.' + )); + $this->assertEqual($expected, $result); + } - public function testFilters() { - $request = new Request(); + public function testFilters() { + $request = new Request(); - $expected = array('message' => 'Access denied.', 'redirect' => '/login'); - $result = Access::check('test_access_with_filters', false, $request, array('redirect' => '/login')); - $this->assertEqual($expected, $result); - } + $expected = array('message' => 'Access denied.', 'redirect' => '/login'); + $result = Access::check('test_access_with_filters', false, $request, array( + 'redirect' => '/login' + )); + $this->assertEqual($expected, $result); + } - public function testNoConfigurations() { - $request = new Request(); - - Access::reset(); - $this->assertIdentical(array(), Access::config()); - $this->expectException("Configuration `test_no_config` has not been defined."); - Access::check('test_no_config', false, $request); - } + public function testNoConfigurations() { + Access::reset(); + $this->assertIdentical(array(), Access::config()); + $this->expectException("Configuration `test_no_config` has not been defined."); + Access::check('test_no_config', false, new Request()); + } } -?> + +?> \ No newline at end of file diff --git a/tests/mocks/extensions/adapter/auth/MockAuthAdapter.php b/tests/mocks/extensions/adapter/auth/MockAuthAdapter.php index d946f3e..10e414b 100644 --- a/tests/mocks/extensions/adapter/auth/MockAuthAdapter.php +++ b/tests/mocks/extensions/adapter/auth/MockAuthAdapter.php @@ -5,19 +5,20 @@ class MockAuthAdapter extends \lithium\core\Object { public function check($credentials, array $options = array()) { - return isset($options['success']) && !empty($credentials->data) ? $credentials->data : false; + $granted = false; + if (isset($options['success']) && !empty($credentials->data)) { + $granted = $credentials->data; + } + return $granted; } - public function set($data, array $options = array()) { - if (isset($options['fail'])) { - return false; - } + public function set($data) { return $data; } public function clear(array $options = array()) { - } + } } -?> +?> \ No newline at end of file