Home

Awesome

PropAuth: Property-based policy evaluation

Travis-CI Build Status

Performing evaluations on credentials for authentication or sets of permissions on users has its limitations. With these things you're restricted to evaluations like "has permission" or "credentials invalid". The goal behind PropAuth is to make these evaluations much more flexible and allow you to define reusable policies that can be evaluated against the provided user dynamically.

Hey Laravel users, there's also a provider to help you integrate PropAuth into your application and Blade templates: PropAuth-Provider

Installation

You can install the library easily with Composer:

composer.phar install psecio/propauth

Examples

<?php

require_once 'vendor/autoload.php';

use \Psecio\PropAuth\Enforcer;
use \Psecio\PropAuth\Policy;

$enforcer = new Enforcer();
$myUser = (object)[
	'username' => 'ccornutt',
    'permissions' => ['test1'],
    'password' => password_hash('test1234', PASSWORD_DEFAULT)
];

$myPolicy = new Policy();
$myPolicy->hasUsername('ccornutt');

$result = $enforcer->evaluate($myUser, $myPolicy);
echo 'RESULT: '.var_export($result, true)."\n\n"; // result is true

// You can also chain the evaluations to make more complex policies
$myPolicy->hasUsername('ccornutt')->hasPermissions('test1'); // also true

// There's also a static method to help make the creation more concise
$myPolicy = Policy::instance()->hasUser('ccornutt')->hasPermissions('test1');

?>

NOTE: All matching is treated as AND so all criteria must be true for the evaluation to pass.

Allowed User Types

The PropAuth engine tries several different ways to get information from the user instance (properties) that should accommodate most of the common User class types out there. When checking for a property, the engine will, in this order:

In the first code example, we're just creating a basic class (stdClass) and applying public properties so it would match with the first check for public properties.

Verifying passwords

You can also use PropAuth to verify passwords as a part of your policy right along with the other evaluations. Here's an example of a policy that would verify the input for the user defined above:

<?php
$myUser = (object)[
    'username' => 'ccornutt',
    'password' => password_hash('test1234', PASSWORD_DEFAULT)
];

$gate = new Gateway($myUser);
$subject = $gate->authenticate($password);

if ($subject !== false && $subject->can('policy1') === true) {
	echo 'They can, woo!';
}

?>

The password validation assumes the use of the password hashing methods and so requires PHP >=5.5 to function correctly. The plain-text password is given to the policy and hashed internally. Then the values are checked against the ones provided in the user for a match. In this case, if they put in either the wrong username or password, the policy evaluation will fail.

How it checks properties

If you'll notice, we've called the hasUsername method on the Policy above despite it not being defined on the User class. This is handled by the __call magic method. It then looks for one of two key words: has or not. It determines which kind of check it needs to perform based on this.

This gives you the flexibility to define custom policies based on the properties your user has and does not restrict it down to just a subset required by the object.

Rules (ANY and ALL)

Your checks can have a second parameter after the value that further customizes the checks that it performs: Policy::ANY and Policy:ALL. These have different meanings based on the data in the property and the data defined. Here's a brief summary:

Property data typeInput Data typeANYALL
stringstringequals (==)equals (==)
stringarrayinput contains propertyall input values equal property
arraystringproperty contains inputall property values equal input
arrayarrayany match of input with property valuesproperty values exactly equals input

NOTE: The Policy::ANY rule is the default, regardless of data input type. All matches are done as exactly equal, even arrays (so if the order of the values is different they won't match).

Searching by "path"

PropAuth also allows you to search for the data to evaluate by a "path". Using this searching, you can recurse down through object and array data to locate just what you want rather than having to gather it before hand. Here's an example:

<?php
class User
{
    public $permissions = [];
    public $username = ['user1', 'user2'];
}

class Perm
{
    public $name = '';
    protected $value = '';
    public $tests = [];

    public function __construct($name, $value)
    {
        $this->name = $name;
        $this->value = $value;
    }
    public function setTests(array $tests)
    {
        $this->tests = $tests;
    }

    public function getValue()
    {
        return $this->value;
    }
}
class Test
{
    public $title;
    public $foo = [];

    public function __construct($title)
    {
        $this->title = $title;
    }
    public function setFoos(array $foo)
    {
        $this->foo = $foo;
    }
}
class Foo
{
    public $test;

    public function __construct($test)
    {
        $this->test = $test;
    }
}

$policies = PolicySet::instance()
    ->add('test', Policy::instance()->find('permissions.tests.foo')->hasTest('t3'));

?>

In this case, we have a lot of nested data under the User instance that we want to evaluate. For this test policy, however, we only want one thing: the "test" values from the Foo objects. To fetch these we'd have to go through this process:

  1. On the User instance, get the permissions property value
  2. For each of the items in this set, get the Test instances that relate to them
  3. Then, for each one of these tests, we want only the Foo instances related in a set.

While this can be done outside of the PropAuth library and just passed in directly for evaluation, the search "path" handling makes it easier. To perform all of the above, you just use the search path in the example above: permissions.tests.foo. This fetches all of the Foo instances then the hasTest method looks at the test value on the Foo objects to see if any match the t3 value.

Other examples

All of the following examples evaluate to true based on the defined user.

<?php

$policy = new Policy();

#### POSITIVE CHECKS

// Check to see if the permission is in a set
$user = new User([
	'permissions' => ['test1', 'test2']
]);
$policy->hasPermissions('test1');

// Check to see if any permissions match
$policy->hasPermissions(['test3', 'test2', 'test5'], Policy::ANY);

// Check to see that the permissions match exactly
$policy->hasPermissions(['test1', 'test2'], Policy::ALL);

#### NEGATIVE CHECKS

// Check to see if the permission is NOT in the set
$policy->notPermissions('test5');

// Check to see if NONE of the permissions match
$policy->notPermissions(['test3', 'test5', 'test6'], Policy::ANY);

// Check to see if the permissions are NOT equal
$policy->notPermissions(['test4', 'test5'], Policy::ALL);
?>

Using Callbacks

If you have some more custom logic that you need to apply, you might want to use the callback handling built into PropAuth. Much like the "has" and "not" of the property checks, there's "can" (result should be true) and "cannot" (result should be false) for callbacks. Here's an example of each:

<?php
// Make a user
$myUser = (object)[
	'username' => 'ccornutt',
	'permissions' => ['test1']
];

// Make a post
$post = (object)[
	'title' => 'This is a test post',
	'id' => 1
];

$myPolicy = new Policy();
$myPolicy
    ->hasUsername(['ccornutt', 'ccornutt1'], Policy::ANY)
    ->can(function($subject, $post) {
		return ($post->id === 1);
    })
    ->cannot(function($subject, $post) {
		return (strpos($post->title, 'foobar') === false);
    });

$result = $enforcer->evaluate($myUser, $myPolicy, [ $post ]); // result is TRUE
?>

NOTE: The additional parameters that are passed in to the evaluate method will be given to the closure check types in the same order they're given in the array. However, the first parameter will always be the subject (User) being evaluated.

Policy Sets

It's also possible to defile a policy in a set, referenced by a key name (string). For example, if we wanted to create a simple policy that let a user with the username "testuser1" be able to perform an "edit post" action:

<?php

$set = new PolicySet();
$set->add(
	'edit-post',
	Policy::instance()->hasUsername('testuser1')
);

// Or, using the instance method
$set = new PolicySet()
	->add('edit-post', Policy::instance()->hasUsername('testuser1'));
?>

Then, when we want to evaluate the user against this policy, we can use the allows and denies methods after injecting the set into the Enforcer:

<?php
$myUser = (object)[
	'username' => 'testuser1',
	'permissions' => ['test1']
];
$enforcer = new Enforcer($set);

if ($enforcer->allows('edit-post', $myUser) === true) {
	echo 'Hey, you can edit the post - rock on!';
}

?>

Using a Class & Method for Evaluation

You can also use a class and method for evaluation as a part of can and cannot checks similar to how the closures work. Instead of passing in the closure method like in the previous examples, you simply pass in a string with the class and method names separated by a double colon (::). For example:

<?php

$policy = Policy::instance()->can('\App\Foo::bar', [$post]);

?>

In this example, you're telling it to try to create an instance of the \App\Foo class and then try to call the bar method on that instance. Note: the method does not need to be static despite it using the double colon. Much like the closures, the subject will be passed in as the first parameter. Additional information will be passed in as other parameters following this.

So, in our above example the method would need to look like this:

<?php
namespace App;

class Foo
{
	public function foo($subject, $post)
	{
		/* evaluation here, return boolean */
	}
}

?>

Loading Policies from an External Source

Defining policies in your code is good, but sometimes it just makes more sense to have them in an external location where you can load them as needed. Maybe you have a situation where only "Post" related policies need to be loaded and not everything across the entire site. PropAuth offers a "load DSL" method on the Policy class that can help here.

What's a DSL? A "domain specific language" lets you define a string in a certain format where PropAuth will understand how to parse it and create a policy instance from it. Let's start with an example and then break down the format:

hasUsername:ccornutt||notUsername:ccornutt1||hasPermissions:(test1,test2)[ANY]

You'll notice that there's similarities in the method names you'd call if you were making the policy yourself and what the DSL will recognize (like hasUsername). This DSL string defines the following policy:

The logic is the same as if you were manually setting those methods on the policy object, just in a simplified way. Here's how it looks in code:

<?php
$dsl = 'hasUsername:ccornutt||notUsername:ccornutt1||hasPermissions:(test1,test2)[ANY]';
$myPolicy = Policy::load($dsl);
?>

Simple right? Okay, so lets look at the format:

Obviously this is only really for simpler checks where the criteria can be defined by strings and simple values, but it can be quite useful for a wide range of circumstances. Of course, you can always pull these as base policies and then add on to them manually once you have the Policy object created post-load.

Using the Gateway interface for evaluation

In addition to the powerful features already listed here, the PropAuth library also provides a simplified interface for working with your user (subject) and its authentication and authorization.

First, let's take a look at authentication. It uses a bcrypt hashing method behind the scenes to evaluate the password:

<?php
$myUser = (object)[
    'username' => 'ccornutt',
    'password' => password_hash('test1234', PASSWORD_DEFAULT)
];

$gate = new Gateway($myUser);
$subject = $gate->authenticate($password);

// Then we can check if the user is authenticated
echo 'Is authenticated? '.var_export($subject->isAuthed(), true)."\n";

?>

We create the Gateway class instance and then can call the authenticate method on it with the password provided. The script then assumes it can access the user's password property as a value on the object and makes a comparison. It will return a new instance of a Subject class if the authentication is successful or false if not. The Subject class is just a wrapper around your object ($myUser in this case). The original object can be fetched using the Subject->getSubject() method.

Additionally, you can provide a bit more context and use the Gateway interface for policy evaluation too. You simply define the policies as a part of the Context object in the constructor:

<?php
$context = new Context([
	'policies' => [
		'policy1' => Policy::instance()->hasUsername('ccornutt')
	]
]);
$gate = new Gateway($myUser, $context);

// When we can call the "evaluate" method with the policies we want to check:
if ($gate->evaluate('policy1') === true) {
	echo 'Policy1 passes!';
}

// Or you can just add your own PolicySet instance and use "evaluate" the same way
$myPolicySet = new PolicySet()->add('edit-post', Policy::instance()->hasUsername('testuser1'));

$context = new Context([
	'policies' => $myPolicySet
]);

?>

Once you have your valid Subject instance, you can then check its abilities with the can and cannot methods:

<?php
$myUser = (object)[
    'username' => 'ccornutt',
    'password' => password_hash('test1234', PASSWORD_DEFAULT)
];

$context = new Context([
	'policies' => [
		'policy1' => Policy::instance()->hasUsername('ccornutt')
	]
]);

$gate = new Gateway($myUser, $context);
$subject = $gate->authenticate($password);

if ($subject !== false && $subject->can('policy1') === true) {
	echo 'They can, woo!';
}

?>

The parameter on the can and cannot methods are policy names you've already defined in your context. If the Policy is defined as a closure with more complex logic, you can provide this option (or multiple options) as the second parameter:

<?php
$post = (object)[
	'author' => 'ccornutt'
];
$set = PolicySet::instance()->add(
    'delete',
    Policy::instance()->can(function($subject, $post) {
        return ($subject->username == 'ccornutt' && $post->author == 'ccornutt');
    })
);
$context = new Context(['policies' => $set]);
$gate = new Gateway($myUser, $context);

$subject = $gate->authenticate('test1234');

if ($subject->can('delete', $post) === true) {
	echo 'They can delete it!';
}
?>