A library for property-based policy evaluation

Overview

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:

  • Look for a public property with the given name
  • Look for a getter with the property name (ex: for "foo" it looks for "getFoo")
  • Look for the "getProperty" method specifically and calls it with the property name

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.

  • has: Tells the system to do an "equals" match, comparing the given value and the property value
  • not: Tells the systems to do a "not equals" match

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 type Input Data type ANY ALL
string string equals (==) equals (==)
string array input contains property all input values equal property
array string property contains input all property values equal input
array array any match of input with property values property 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:

  • subject has the username of "ccornutt"
  • subject does not have the username of "ccornutt1"
  • subject has any of the following permissions: test1, test2

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:

  • the method/value pairs are split by the double pipe (||)
  • the method name and value are then split by a colon (:)
  • single string values are just put into the string, arrays are surrounded by parentheses and split with a comma
  • modifiers (like ANY or ALL) are added to the end of the method/value pair, enclosed in brackets ([])

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!';
}
?>
You might also like...
KLua is a FFI-based Lua5 library that can be used in both PHP and KPHP
KLua is a FFI-based Lua5 library that can be used in both PHP and KPHP

KLua KLua is a FFI-based Lua5 library that can be used in both PHP and KPHP. Installation Since this is a FFI library, it needs a dynamic library avai

A small library for validating International Bankaccount Numbers (IBANs) based on the IBAN Registry provided by SWIFT

A small library for validating International Bankaccount Numbers (IBANs) based on the IBAN Registry provided by SWIFT

Websocket chat room written in PHP based on workerman.

基于workerman的GatewayWorker框架开发的一款高性能支持分布式部署的聊天室系统。

Skosmos is a web-based tool providing services for accessing controlled vocabularies, which are used by indexers describing documents and searchers looking for suitable keywords.

Skosmos is a web-based tool providing services for accessing controlled vocabularies, which are used by indexers describing documents and searchers looking for suitable keywords.

Rules to detect game engines and other technologies based on Steam depot file lists

SteamDB File Detection Rule Sets This is a set of scripts that are used by SteamDB to make educated guesses about the engine(s) & technology used to b

Plant Watering Sensor Project for Zigbee Network (based on the Source Code of the DIYRUZ Flower Project  - https://github.com/diyruz/flower).
Plant Watering Sensor Project for Zigbee Network (based on the Source Code of the DIYRUZ Flower Project - https://github.com/diyruz/flower).

Plant-Watering-Sensor-Zigbee Plant Watering Sensor Project for Zigbee Network (based on the Source Code of the DIYRUZ Flower Project

Create changelogs based on your GitHub milestones

changelog_generator.php This project provides a simple way to create a markdown ordered list of issues and pull requests closed with a given milestone

Spotweb is a decentralized usenet community based on the Spotnet protocol.

Spotweb is a decentralized usenet community based on the Spotnet protocol. Spotweb requires an operational webserver with PHP5.6 installed, it

Comments
  • Make the

    Make the "property value not found" error message clearer

    In the Enforcer class there's a message talking about the property value being invalid. What this actually means is that the property couldn't be accessed on the object (the subject) so the evaluation can't happen. This throws an exception right now but the message is a bit cryptic.

    This could probably be made clearer.

    Hacktoberfest 
    opened by enygma 0
Owner
psec.io
psec.io
S11 Selection est une solution web PHP faite pour automatiser la création d'une grille d'évaluation d'un étudiant puis de les rassembler pour en faire un classement.

[S11] SELECTION BTS 0.1.0 FR Description S11 Selection est une solution web PHP faite pour automatiser la création d'une grille d'évaluation d'un étud

NOIZET Maxence 1 Oct 25, 2022
provides a nested object property based user interface for accessing this configuration data within application code

laminas-config This package is considered feature-complete, and is now in security-only maintenance mode, following a decision by the Technical Steeri

Laminas Project 43 Dec 26, 2022
PHP package to make your objects strict and throw exception when you try to access or set some undefined property in your objects.

?? Yell PHP package to make your objects strict and throw exception when you try to access or set some undefined property in your objects. Requirement

Zeeshan Ahmad 20 Dec 8, 2018
A text-based, persistent browser-based strategy game (PBBG) in a fantasy war setting

Note: OpenDominion is still in development. Some features of the game have not been implemented yet. Introduction OpenDominion is a free and open-sour

null 180 Dec 28, 2022
Small convention based CQRS library for PHP

LiteCQRS for PHP Small naming-convention based CQRS library for PHP (loosely based on LiteCQRS for C#) that relies on the MessageBus, Command, EventSo

Benjamin Eberlei 560 Nov 20, 2022
Currency is a simple PHP library for current and historical currency exchange rates & crypto exchange rates. based on the free API exchangerate.host

Currency Currency is a simple PHP library for current and historical currency exchange rates & crypto exchange rates. based on the free API exchangera

Amr Shawky 19 Dec 12, 2022
Deeper is a easy way to compare if 2 objects is equal based on values in these objects. This library is heavily inspired in Golang's reflect.DeepEqual().

Deeper Deeper is a easy way to compare if 2 objects is equal based on values in these objects. This library is heavily inspired in Golang's reflect.De

Joubert RedRat 4 Feb 12, 2022
Small library providing some functional programming tools for PHP, based on Rambda

Functional library for PHP. Features: set of useful functions helpful in functional programming all functions are automatically curried every array ca

Wojciech Nawalaniec 5 Jun 16, 2022
A comprehensive library for generating differences between two strings in multiple formats (unified, side by side HTML etc). Based on the difflib implementation in Python

PHP Diff Class Introduction A comprehensive library for generating differences between two hashable objects (strings or arrays). Generated differences

Chris Boulton 708 Dec 25, 2022
PHP library for generating random avatars based on avataaars

PHP Avataaar PHP library for generating random avatars based on avataaars. Installation Dependencies PHP 8.0 Composer 2.0 Install Install the library

John Ciacia 2 Feb 27, 2022