Skip to content

Policy based authorization

Marin Călin edited this page Sep 20, 2022 · 2 revisions

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.

Requirements

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.

Requirement Handlers

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.

Applying to actions

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)