Code Monkey home page Code Monkey logo

li3_access's Introduction

Access control library for the Lithium framework.

Installation

Checkout the code to either of your library directories:

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');

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', $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 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

In this repository there are three adapters. All three work in a slightly different way.

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')
);

And that's it!

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')
);

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']);
}

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.
});

Then to use your new rule:

$access = Access::check('rules', Auth::check('auth_config_name'), $this->request, array('rule' => 'custom_rule'));

One more to go!

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 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',

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.

'message'

Overwrites the default message to display if the rule matches the request and is disallowed.

'redirect'

Overwrites the default redirect to use if the rule matches the request and is dissallowed.

'match'

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.

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:

Auth::config(array(
	'user' => array(
		'adapter' => 'Form',
		'model' => 'User',
		'fields' => array('email' => 'email', 'password' => 'password'),
		'scope' => array('active' => true)
	),
	'editor' => array(
		'adapter' => 'Form',
		'model' => 'Editor',
		'fields' => array('email' => 'email', 'password' => 'password'),
		'scope' => array('active' => true, 'group' => 1)
	),
	'customer' => array(
		'adapter' => 'Form',
		'model' => 'Customer',
		'fields' => array('email' => 'email', 'password' => 'password'),
		'scope' => array('active' => true, 'group' => 2)
	)
));

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'

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.

Finally, if you pass either $request or $options you can modify their values at runtime.

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);
			}
		)
	)
));

Credits

Tom Maiaroto

The original author of this library.

Github: tmaiaroto

Website: Shift8 Creative

Weluse

Wrote the original Rbac adapter.

Github: Marc Schwering weluse

Website: Weluse

rich97

Modified the original Rbac adapter, added some tests and wrote this version of the documentation.

Github: rich97

Website: Enrich.it

li3_access's People

Contributors

joedevon avatar mariano avatar nateabele avatar rich97 avatar tmaiaroto avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

li3_access's Issues

AuthRbac should accept a closure in 'allow', not only arrays

More often than not when you use the 'allow' option in an AuthRbac rule to specify your own closure, you only use one closure. Currently such a rule has to be written as:

array(
    'requesters' => '*',
    'match' => 'Users::signup',
    'allow' => array(
        function($request, $options) {
            return !Auth::check('default');
        }
    )
)

Ideally you want to write this without the array, as such:

array(
    'requesters' => '*',
    'match' => 'Users::signup',
    'allow' => function($request, $options) {
        return !Auth::check('default');
    }
)

Coming up a pull request with change + tests to improve this.

AuthRbac::parseMatch() can't deal with namespaces

This is a biggie. parseMatch()'s regex can't deal with namespaces, which makes (for example) the test suite impossible to add a rule for. Ideally you want to be able to do:

Access::config(array('default' => array(
    'adapter' => 'AuthRbac',
    'roles' => array(
        array(
            'requesters' => 'devs'
            'match' => 'lithium\test\Controller::*'
    )
));

But naturally this affects any namespace you wish to use, making it impossible to deal with more than one controller named the same way, but with different namespace.

Also, on a small note, the preg_match() can take the controller and action right from the match, without the need for the extra explode() call.

Coming up a pull request with a fix and tests for the fix.

li3_access does not pay attention to Dispatcher rules

Here's the scenario. If you have defined, somewhere in your bootstrap config, rules of Dispatcher configurations, like so:

Dispatcher::config(array('rules' => array(
    'admin' => array('action' => 'admin_{:action}'
)));

Access, and all the access adapters, do not take into account any modifications made to the route parameters. This is because Dispatcher::applyRules() makes the modifications to the $params array, instead of Request::$params. This is not going to change (double checked it with @nateabele), so we need a way for these dispatcher rules to be taken into account.

Before proceeding with the solution I implemented (coming up in a pull request in just a few minutes), let me give you a use case. You define a modification just as the one above so when the 'admin' route parameter is one, the action gets prefixed with admin_. You then add the appropriate route continuation so that this route parameter is set when an URL is accessed with /admin as a prefix:

Router::connect('/admin/{:args}', array('admin' => true), array('continue' => true));

Let us now add an auth rule (for the AuthRbac adapter), so that all admin prefixed accesses are only allowed access when the appropriate prefix is set in Auth. Now, a note here. I could easily change the following check to use $request->params['admin'] which would work just fine, however I'm trying to showcase a scenario where you would need to do something with the params after they have been modified by a dispatcher rule. Also, you'll see that I'm using a closure (instead of array of closures) directly in the match setting, the fix for this is also part of the pull request:

Auth::config(array('default' => array(
    'adapter' => 'AuthRbac',
    'roles' => array(
        array(
            'requesters' => '*',
            'match' => function($request) {
                return (strpos('admin_', $request->params['action']) === 0);
            },
            'allow' => function() {
                return Auth::check('admin');
            }
        )
    )
)));

The above code would never match since the $request->params used are the parameters parsed before applying any dispatcher rules.

To fix this, and as per @nateabele suggestion, I refactored li3_access so instead of passing the request when filtering Dispatcher::_callable, one can directly pass the $params. However, I made it backwards compatible, so code passing the Request instance should still work.

The match closure now receive an extra parameter which is the whole $params array. So after applying the pull request, you would first change how you call Access when filtering Dispatcher::_callable:

Dispatcher::applyFilter('_callable', function($self, $params, $chain) {
    $access = Access::check('default', null, $params);
    // ...
});

and the above 'match' would be fixed by changing it to:

'match' => function($request, $params) {
    return (strpos('admin_', $params['action']) === 0);
}
)));

As part of the pull request I also had to adapt the test cases that were using adapter methods directly.

Optional/boolean route parameters not matched

I have an admin route and sometimes the admin param isn't specified (admin = null vs admin = true) in the route. With AuthRbac, this causes an issue when parsing the match. My fix is as follows but I haven't thoroughly tested it:

Line 147 AuthRbac.php, change

        if (!$exists_in_request || $value !== Inflector::underscore($request->params[$type])) {
            return false;
        }

to:

        if($value == null && !$exists_in_request) {
          continue;
        }
        if(!$exists_in_request || $value !== (is_string($request->params[$type]) ? Inflector::underscore($request->params[$type]) : $request->params[$type])) {
          return false;
        }

I didn't create a pull request because I also want to verify that there isn't an alternate way of doing this that I haven't realized?

Usage of Auth::check in AuthRbac makes signup forms log in user

Ok consider this scenario:

  1. You build a signup form where you allow users to specify email and password:
<h1>Signup</h1>
<?php echo $this->form->create(isset($user) ? $user : null); ?>
    <?php echo $this->form->field('email'); ?>
    <?php echo $this->form->field('password', array('type' => 'password')); ?>
    <?php echo $this->form->field('name'); ?>
    <?php echo $this->form->submit('Signup'); ?>
<?php echo $this->form->end(); ?>
  1. You configure Auth so fields is array('email', 'password'), and set it to use theForm` adapter:
Auth::config(array('default' => array(
    'adapter' => 'Form',
    'model' => 'app\models\User',
    'fields' => array('email', 'password')
)));
  1. You add an AuthRbac rule so that the signup form is only accessible by non-logged-in users (this step is not really necessary to make the point this ticket is making, but just for illustration purposes):
Access::config(array('default' => array(
    array(
        'requesters' => '*',
        'match' => 'Users::signup',
        'allow' => function() {
            return !Auth::check('default');
        }
    )
)));

Now access the signup form, and enter an existing email & password into the corresponding fields. If you have proper validation in place, you'd get your validation fields (because of duplicated email). However, you may notice that you are now logged in.

This is because in AuthRbac::_getRolesByAuth(), when calling Auth::check() the actual request object is being passed on to Auth. When you pass the request object to Auth, then the Form adapter can check for the submitted fields and look for a valid record, thus logging you in right from a signup form. We obviously don't want that.

A way to avoid this is to "emptying" the data array from the Request before doing the access check. So when filtering Dispatcher::_callable you can do:

$_data = $params['request']->data;
$params['request']->data = array();
$access = Access::check('default', null, $params);
$params['request']->data = $_data;

But obviously this is not so clean. I could easily make a pull request to avoid passing the Request instance to Auth::check() from the AuthRbac adapter, but I wanted to double check with you before doing so, because you may have had a purpose for passing it which I might be missing.

Also, is there any reason why the $requester argument to AuthRbac::check() is not being used?

Maybe I don't understand what this lines does, but...

$rules = (isset($options['rules']['rule'])) ? array($options['rules']) : $options['rules'];

So if $options['rules']['rule'] exists, which means $options['rules'] is an array, then cast $options['rules'] to an array and pass that value to $rules.

Otherwise, clearly $options['rules'] is not an array, therefore just pass it to $rules.

Isn't that the same as:

$rules = $options['rules']

?

Am I missing something?

(Sometimes my tone gets misunderstood on the Internet so to be clear, I'm not being snarky. Just want to clarify.)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.