-
Notifications
You must be signed in to change notification settings - Fork 0
Policy based authorization
Underneath the covers, role-based authorization and permission-based authorization use a requirement, a requirement handler, and a preconfigured policy. These building blocks support the expression of authorization evaluations in code. The result is a richer, reusable, testable authorization structure.
An authorization policy consists of one or more requirements, defined in a custom class, usually inside the App\Authorization\Policies
namespace.
For example, the AtLeast21Policy
is created. It has a single requirement — that of a minimum age, which is supplied as a parameter to the requirement.
<?php
namespace App\Authorization\Policies;
use Codestage\Authorization\Contracts\IPolicy;
use Codestage\Authorization\Contracts\IRequirement;
use App\Authorization\Requirements\MinimumAgeRequirement;
class AtLeast21Policy implements IPolicy
{
/**
* The list of requirements that need to be fulfilled in order to complete this policy.
*
* @return array<int, IRequirement>
*/
public function requirements(): array
{
return [
new MinimumAgeRequirement(21)
];
}
}
Note 1: Policies can be easily created by using the
php artisan make:policy <name>
command.
Note 2: This package overrides the default Laravel policy creation command. While you can technically still use Laravel policies along with the new ones, we recommend not mixing and matching between them.
An authorization requirement is a collection of data parameters that a policy can use to evaluate the current authenticatable model. In our AtLeast21Policy, the requirement has a single parameter — the minimum age.
A requirement implements the IRequirement
contract, which is an empty marker interface.
A parameterized minimum age requirement could be implemented as follows:
<?php
namespace App\Authorization\Requirements;
use Codestage\Authorization\Attributes\HandledBy;
use Codestage\Authorization\Contracts\IRequirement;
use App\Authorization\Handlers\MinimumAgeRequirementHandler;
#[HandledBy(MinimumAgeRequirementHandler::class)]
class MinimumAgeRequirement implements IRequirement
{
public function __construct(public readonly int $age)
{
}
}
Note: Requirements can be easily created by using the
php artisan make:requirement <name>
command, which also generates a requirement handler.
If an authorization policy contains multiple authorization requirements, all requirements must pass in order for the policy evaluation to succeed. In other words, multiple authorization requirements added to a single authorization policy are treated on an AND basis.
The following example shows a minimum age handler that handles the MinimumAgeRequirement
:
<?php
namespace App\Authorization\Handlers;
use Codestage\Authorization\Contracts\IRequirement;
use Codestage\Authorization\Contracts\IRequirementHandler;
use App\Authorization\Requirements\MinimumAgeRequirement;
use Illuminate\Contracts\Auth\Guard;
/**
* @implements IRequirementHandler<MinimumAgeRequirement>
*/
class MinimumAgeRequirementHandler implements IRequirementHandler
{
/**
* MinimumAgeRequirementHandler constructor method.
*
* @param Guard $_authManager
*/
public function __construct(private readonly Guard $_authManager)
{
}
/**
* Check whether the requirement this class handles is passing.
*
* @param MinimumAgeRequirement $requirement
* @return bool
*/
public function handle(IRequirement $requirement): bool
{
return $this->_authManager->user()->age >= $requirement->age;
}
}
The preceding code has the Guard
contract injected in its constructor method, and then uses it to check if the user's age is greater than or equal to the required age and returns the result inside the handle
method.
As you may have noticed in the Requirement class, the handler for that requirement is defined by using the HandledBy
attribute.
This makes it possible to define a handler for multiple requirements.
Policy authorization can be applied to actions and controllers, using the Authorize
attribute:
#[Authorize(AtLeast21Policy::class)]
class DangerousController
{
public function __invoke()
{
//
}
}
Sometimes, you may need parameters in order to check for access. For example, you could require a variable age for your policy:
#[Authorize(AtLeastAgePolicy::class, ['age' => 21])]
class DangerousController
{
public function __invoke()
{
//
}
}
And inside your policy, add an 'age' parameter to your constructor:
<?php
namespace App\Authorization\Policies;
use Codestage\Authorization\Contracts\IPolicy;
use Codestage\Authorization\Contracts\IRequirement;
use App\Authorization\Requirements\MinimumAgeRequirement;
class AtLeastAgePolicyimplements IPolicy
{
/**
* AtLeastAgePolicyimplements constructor method.
*
* @param int $age
*/
public function __construct(public readonly int $age)
{
}
/**
* The list of requirements that need to be fulfilled in order to complete this policy.
*
* @return array<int, IRequirement>
*/
public function requirements(): array
{
return [
new MinimumAgeRequirement(21)
];
}
}
When applied to actions, you may want to pass route parameters to policies. For example:
class SomeController extends Controller
{
#[Authorize(PolicyThatRequiresUserProfile::class, ['profile'])]
public function __invoke(UserProfile $profile)
{
//
}
}
Note: This will cause route bindings to be before the policy is run. If the profile in the above example does not exist in the database, the authorization will fail. (
AuthorizationException
thrown)