# Policy Manager

While nothing stops you from using other components of the framework directly, the policy manager JsonPolicy\Manager is the main and ideally the only class that you should work with.

The main objective for the policy manager is to pre-parse the collection of policies and prepare a response to the methods like isDenied, isAllowedTo or getParam while taking into consideration all the defined conditions.

FYI! By default the framework supports "allow" and "deny" effects that can be evaluated by isAllowed, isAllowedTo, isDenied and isDeniedTo methods. However, you can define your custom effects with custom_effects option.

# ->is...()

In order to check if any given resource or combination of resource/action can be invoked, the framework offers distinct methods with two overloads each:

// Check given resource
is<Adjective>(mixed $resource, mixed $args = []): bool|null
is<Adjective>(mixed $resource, bool $default = null, mixed $args = []): bool

// Check given resource and action
is<Adjective>To(mixed $resource, string $action, mixed $args = []): bool|null
is<Adjective>To(mixed $resource, string $action, bool $default = null, mixed $args = []): bool

# Parameters

$resource
The resource that is evaluated. It can be anything as long as you have a way to convert it to a string name. If the resource is an object, by default, the framework converts it to class name with get_class(opens new window) PHP function. You can register custom interpreters with the custom_resources setting.

$action
A string value for the action that can be performed upon the resource.

$default
An optional boolean value that is returned if no applicable statements are found. The applicable statement is the one that targets provided resource and conditions are evaluated to true.

$args
The optional parameter that contains a non-boolean value that is added to the context and can be referred to in a policy with ${ARGS.<path>} marker.


The only difference between is<Adjective> and is<AdjectiveTo> is that the second method expects the second argument to be a string value for the action.

Note! Please don't get this verbosity to confuse you. The only reason all this exists is to give you the ability to be more granular with policies. Not everything is only "allow" and "deny". Sometimes it is more intuitive to use effects like "restrict", "attach", "apply", etc.

In the example below, we check if a specific page is restricted based on the policy.

$manager = PolicyManager::bootstrap([...]);
$page    = new PageEntity([
    'id'   => 123,
    'slug' => 'some-page',
    'cats' => [
        'restricted',
        'science'
    ]
    ....
]);

...

if ($manager->isRestrictedTo($page, 'view', true)) {
    echo 'Yes you are allowed to view this page';
}

{
    "Statement": {
        "Effect": "restrict",
        "Resource": "PageEntity",
        "Action": [
            "view",
            "edit"
        ],
        "Condition": {
            "In": {
                "restricted": "(*array)${PageEntity.cats}"
            }
        }
    }
}

# ->getParam()

Get applicable param from the defined policies.

getParam(string $key, mixed $default = null, mixed $args = []): mixed

# Parameters

$key
A string value for the param's Key.

$default
The optional default value is returned if no applicable params are found. By default, the null value is returned.

$args
The optional parameter that value is going to be added to the context and can be referred to in a policy with ${ARGS.<path>} marker.


In the example below, we determine the proper API endpoint based on the environment variable APP_ENV.

use JsonPolicy\Manager;

$manager = Manager::bootstrap([
    'policies' => [
        file_get_contents(__DIR__  . '/policy.json')
    ]
]);

print_r($manager->getParam('API:endpoint'));
{
    "Param": [
        {
            "Key": "API:endpoint",
            "Value": "https://staging-myapi.mydomain.io",
            "Condition": {
                "Equals": {
                    "(*int)${ENV.APP_ENV}": "staging"
                }
            }
        },
        {
            "Key": "API:endpoint",
            "Value": "https://myapi.mydomain.io",
            "Condition": {
                "Equals": {
                    "(*int)${ENV.APP_ENV}": "production"
                }
            }
        }
    ]
}

# ::bootstrap()

Bootstrap the policy manager instance. Multiple independent instances can be instantiated with different settings and used either in global or local application scopes.

bootstrap(array $settings = []): JsonPolicy\Manager

# Parameters

$settings
The array of various settings that configure the policy manager's behavior. More detail about each supported setting below.


The bootstrap method accepts an optional associated array of settings where each value can be either array or Closure that returns an array. Below is the list of all recognizable settings.

use JsonPolicy\Manager as PolicyManager;

$manager = PolicyManager::bootstrap([
    'context'           => [...],
    'custom_markers'    => [...],
    'custom_types'      => [...],
    'custom_conditions' => [...],
    'custom_resources'  => [...],
    'custom_effects'    => [...],
    'policies'          => [...]
]);

# context

  • Type: Array|Object|Closure

The context may contain any type of data as long as it is organized as a traversable value or Closure that returns a traversable value. The context is passed alone to parsers and used typically during markers evaluation.

For example, in the sample below, we pass in the context some information about the current user and in our policies, we refer to this information through the ${ARGS.<xpath>} marker.

$manager = PolicyManager::bootstrap([
    'context' => [
        'args' => [
            'user' => [
                'id'       => 1,
                'location' => [
                    'city' => 'New York'
                ]
            ]
        ]
    ]
]);

The policy below defines conditional param that is applicable only if the user's id equals 1 or the user's city is whitelisted.

{
    "Param": {
        "Key": "allow-upgrade",
        "Value": true,
        "Condition": {
            "Operator": "OR",
            "Equals": {
                "(*int)${ARGS.user.id}": 1
            },
            "In": {
                "${ARGS.user.location.city}": [
                    "New York",
                    "Paris"
                ]
            }
        }
    }
}

Finally in the code, the allow-upgrade flag can be fetched easily without bothering about any conditional logic.

if ($manager->getParam('allow-upgrade') === true) {
    echo 'You can do upgrade';
}

# custom_markers

  • Type: Array|Closure

By default, the framework supports only a few markers, however, it can be easily extended to an unlimited number of custom markers.

The custom_markers should be resolved to the associated array of key/value pairs where the key is a case-sensitive marker and value is the valid callback(opens new window) .

The defined callback accepts two arguments and may return any value.

callback_function(string $path_to_property, JsonPolicy\Core\Context $context): mixed

In the sample below, we define a custom marker USER that tries to get current user property

use JsonPolicy\Core\Context;

$manager = PolicyManager::bootstrap([
    'custom_markers' => [
        'USER' => function($prop, Context $context) {
            return MyApp::getCurrentUser()->{$prop};
        }
    ]
]);

In the policy, you can refer to the current user instance with ${USER.<prop>} marker.

{
    "Statement": {
        "Effect": "deny",
        "Resource": "Coupon",
        "Condition": {
            "Equals": {
                "${USER.full_name}": "John Smith"
            }
        }
    }
}

# custom_types

  • Type: Array|Closure

The policy typecasting is critical to cast raw values to the type that is most suited for a statement or param. The framework covers widely used types of data, however, sometimes it is useful to convert values into a custom format.

The custom_types should be resolved to the associated array of key/value pairs where the key is a case-sensitive type name and value is a valid callback(opens new window) .

The defined callback accepts one argument and may return any value.

callback_function(mixed $value): mixed

In the example below, we define a custom typecast md5 that creates an MD5 hash from a given value and this is used in the condition later.

use JsonPolicy\Core\Context;

$manager = PolicyManager::bootstrap([
    'custom_types' => [
        'md5' => function($value) {
            return md5($value);
        }
    ]
]);
{
    "Statement": {
        "Effect": "allow",
        "Resource": "File",
        "Action": [
            "update",
            "rename"
        ],
        "Condition": {
            "Equals": {
                "(*md5)${ARGS.token}": "098f6bcd4621d373cade4e832627b4f6"
            }
        }
    }
}
if ($manager->isAllowedTo($fileObject, 'update', ['token' => 'U893L0'])) {
    // Do something here
}

# custom_conditions

  • Type: Array|Closure

Provide a collection of custom types of conditions that either declare new types of conditions.

The custom_conditions should be resolved to the associated array of key/value pairs where the key is a case-sensitive type name and value is a valid callback(opens new window) .

The defined callback accepts three arguments and has to return a single boolean value.

callback_function(array $group, string $operator, ConditionManager $manager): boolean

In the example below, we declare a new condition Similar that checks if two strings are similar. The two strings are considered to be similar if they are more than 60% similar. This way we prevent a user from publishing content if it is at least 60% similar to one of the articles from the collection of articles.

// Instantiating policy manager
$manager = PolicyManager::bootstrap([
    'custom_conditions' => [
        'Similar' => function($group, $operator, $manager) {
            $result = null;

            foreach ($group as $cnd) {
                $sub_result = null;

                foreach($cnd['right'] as $value) {
                    $sim = 0;
                    similar_text($cnd['left'], $value, $sim);

                    $sub_result = $manager->compute($sub_result, ($sim > 60), 'OR');
                }

                $result = $manager->compute($result, $sub_result, $operator);
            }

            return $result;
        }
    ],
    'policies' => [...]
]);

// This is the article that user is trying to publish
$article = new Article([
    'content' => '...',
    'title'   => 'My original article',
    ...
]);

// Fetching raw array of articles' content from some data source
$articles = MyAPI::fetchArticles();

if ($manager->isAllowedTo($article, 'publish', true, ['articles' => $articles])) {
    echo 'Yes. Your article is actually original';
} else {
    echo 'Ooops. Looks like your article is quite similar to some other article';
}
{
    "Statement": {
        "Effect": "deny",
        "Resource": "Article",
        "Action": "publish",
        "Condition": {
            "Similar": {
                "${Article.content}": "(*array)${ARGS.articles}"
            }
        }
    }
}

# custom_resources

  • Type: Array|Closure

The ->is..() methods expect the first argument to be a resource that is evaluated. The resource can be either an object that is converted to a string or a simple string value that is taken as-is. In the first case, by default, the object's class name is used and this is not always ideal. That is why you can declare a custom callback that takes the evaluating resource and convert it into a string alias.

It is important to identify the resource's alias so it can be matched with the name defined in a statement's Resource attribute.

The callback accepts three arguments and has to return a scalar value or null if the provided resource does not match an expected value.

callback_function(mixed $name, mixed $resource, Manager $manager): mixed

In many cases, the resource's alias has to be computed dynamically based on its properties. For example, let's say you have a page object that is identified by the unique ID and some unique slug. This way you can prepare a policy that targets a very specific page and define a custom callback that converts the page object to its resource alias.

// Instantiating policy manager
$manager = PolicyManager::bootstrap([
    'custom_resources' => [
        function($name, $resource, $manager) {
            // It is a good practice to check if $name is still empty and provided
            // resource matches expected class name. This way you do not override
            // name if was already set by some other custom_resource callback
            if (is_null($name) && is_a($resource, 'Page')) {
                $name = "Page:{$resource->slug}";
            }

            return $name;
        }
    ],
    'policies' => [...]
]);

$page = new Page([
    'ID' => 124,
    'slug'   => 'private-portal',
    ...
]);

if ($manager->isAllowed($page, true)) {
    echo 'Yes. You can access this page';
} else {
    echo 'No. You cannot access this page';
}
{
    "Statement": {
        "Effect": "deny",
        "Resource": "Page:private-portal"
    }
}

# custom_effects

  • Type: Array|Closure

To improve policy readability it is not always intuitive to use only "allow" and "deny" effects. Sometime you might want to use verbs like "apply", "restrict" or "hide". Then you can be more verbose in your code by using methods like isApplied, isRestricted, or isHidden.

To define custom effects, simply provide an associated array of key/value pairs or an anonymous function that returns such array. It is recommended for keys to use adjectives and values have to match effects that are used in policies.

For example, below we build a simple application for boy scouts, were based on logged-in member, we determine what badges are attached to him.

$manager = PolicyManager::bootstrap([
    'policies' => [...],
    'custom_effects' => [
        'attached' => 'attach'
    ]
]);

$member = new Member([
    'name'   => 'John Smith',
    'groups' => [
        'advanced',
        'rock-climber'
    ]
]);

$badges = [
    'super-badge',
    'advanced-badge',
    'smart-badge'
];

foreach($badges as $badge) {
    if ($manager->isAttached($badge, false, $member)) {
        echo "The {$badge} is attached\n";
    } else {
        echo "The {$badge} is not attached\n";
    }
}
{
    "Statement": [
        {
            "Effect": "attach",
            "Resource": "super-badge",
            "Condition": {
                "In": {
                    "superstar": "(*array)${ARGS.groups}"
                }
            }
        },
        {
            "Effect": "attach",
            "Resource": "advanced-badge",
            "Condition": {
                "In": {
                    "advanced": "(*array)${ARGS.groups}"
                }
            }
        },
        {
            "Effect": "attach",
            "Resource": "smart-badge",
            "Condition": {
                "In": {
                    "smart": "(*array)${ARGS.groups}"
                }
            }
        }
    ]
}

# policies

  • Type: Array|Closure

Array or Closure that returns the array of JSON policies. It is outside of the framework's scope to determine a list of policies that should be attached to the current user or process. When you instantiate the JSON policy manager, the list of policies should be already defined.

It is also outside of the framework's scope to determine where policies are stored. They can be part of the project or located on the remote server. In the example below, we fetch a policy from the public Github repository.

$manager = PolicyManager::bootstrap([
    'policies' => function() {
        return [
            file_get_contents('https://raw.githubusercontent.com/jsonpolicy/jsonpolicy-php/master/samples/remote-policy-repository/policy.json')
        ];
    }
]);

if ($manager->isAllowed('registration', true)) {
    echo "The registration is available\n";
    echo "Registration endpoint is " . $manager->getParam('registration-endpoint');
} else {
    echo 'No, the registration is disabled';
}
{
    "Statement": {
        "Effect": "allow",
        "Resource": "registration"
    },
    "Param": {
        "Key": "registration-endpoint",
        "Value": "https://myappdomain.io/register"
    }
}

The example may explain why the framework is described as a distributed rule engine because you can define a centralized location for policies and distribute them across multiple applications disregarding if they use the same programming language or not.

The example also shows how you can design a simple feature flag(opens new window) system or distributed configurations engine. The policy params are great candidates for this.