API Resource Authorization

Directory Structure

The Support Directory

Elentra API 3.1 (Elentra ME 1.16.1+) introduces the Support directory to the top level of the API. This directory is home to the reusable "framework-like" components that are critical to supporting the Elentra API and internal developer APIs. In an effort to increase navigability for developers, this directory has been structured in a way that closely resembles Laravel's Illuminate framework (which the Elentra API is built on top of).

In Elentra API 3.1, new modernized base classes have been provided which can be extended to manage permissions and authorization of API resources. They can be found in the Support\Acl namespace within the support directory. Additionally a Facade wrapper has also been provided to simplify interacting with the Zend ACL library in a way that integrates better with Laravel applications.

app/
└── Support/
    ├── Acl/ 
    |   ├── Assertion.php
    |   ├── Policy.php
    |   └── Resource.php
    └── Facades/
        └── Acl.php

Resources & Assertions

ACL resources and assertions were have been historically defined entrada_acl.inc.php file. This has changed with Elentra API 3.1+ as they can now be defined inside your API modules. Create a new directory called Acl in our sandbox module. Inside of the Acl directory create a new directory named Resources and another called Assertions. Classes that you define in these directories should extend the base Assertion and Resource classes used by our SandboxPolicy class.

app/
└─ Modules/
    └── Sandboxes/
        ├── Acl/
        |   ├── Assertions/
        |   |   └── SandboxOwnerAssertion.php
        |   └── Resources/
        |       └── SandboxResource.php  
        ├── Models/
        |   └── Sandbox.php
        ├── Policies/
        |   └── SandboxPolicy.php
        └── Providers/
            └── AuthServiceProvider.php

Defining Permissions

As of Elentra ME 1.15, the Entrada Zend ACL "MOM array" has been replaced with entries in a series of tables within the auth database.

ACL Resource Types

Begin by inserting a new entry into the acl_lu_resource_types for our new resource type , in this example we will be adding support for authorizing sandbox resources.

INSERT INTO `acl_lu_resource_types` (`resource_type`, `active`) VALUES ('sandboxes', 1);

ACL Inheritance

In order for our resource to be included in the ACL permission tree, we must also add an entry to the acl_resource_inheritance table.

Because the sandbox resource is not a child of another resource, we do not specify a value for the acl_resource_type_parent_id column.

INSERT INTO `acl_resource_inheritance` (`acl_resource_type_id`, `acl_resource_type_parent_id`) VALUES (SELECT LAST_INSERT_ID(), NULL);

ACL Permissions

We must add a rule to the acl_permissions table for each role or permission we intend to give access to a resource. The value of the resource_type field must be identical to the resource type that we added to the acl_lu_resource_types table. The entity type refers to the combination of groups and roles that we would like to allow access to the resource. Here we have chosen to give access to all administrators who are part of the medtech group. Optionally we can specify the fully qualified name of an assertion class that will perform further authorization checks. Finally, we set the flags for each CRUD permission we would like users to have.

The fully qualified class name should not have Assertion at the end, this will be added automatically by the ACL framework.

INSERT INTO `acl_permissions` (`resource_type`, `entity_type`, `entity_value`, `assertion`, `create`, `read`, `update`, `delete`) VALUES ('sandbox', 'group:role', 'staff:staff', 'Entrada\\Modules\\Sandboxes\\Acl\\Assertions\\SandboxOwner', 1, 1, 1, 1);

Defining Resources

Resource classes are required by Zend ACL in order to resolve the type of resource that is attempting to be authorized. They also allow the developer to specify whether or not the rules should be applied to all resources of a certain type or only a specific instance. In our Sandbox API module we must create a new class called SandboxResource that extends the base Resource class in the support directory. We can then override the getResourceId method to return the value of either the general or the specific resource identifier.

<?php

namespace Entrada\Modules\Sandboxes\Acl\Resources;

use Entrada\Support\Acl\Resource;


class SandboxResource extends Resource
{

    /**
     * ACL method for keeping track. Required by \Zend\Permissions\Acl\Resource\ResourceInterface.
     *
     * @return string
     */
    public function getResourceId()
    {
        return 'sandboxes' . ($this->specific ? $this->resource_id : '');
    }

}

Creating Assertions

Assertions classes provide additional checks to verify whether the specific user has access to the specific resource that is being authorized. In Elentra API 3.1, Eloquent and query builder are now fully supported when defining assertions inside of API an module. In our Sandbox API module we must extend the base Assertion class in the support directory. We then override the isUserAllowed method which receives the user that is being authorized; the ID of the resource; the current authorization context; and the CRUD permission that is being checked. isUserAllowed must either return true or false depending on whether the user has the necessary privileges to access the resource.

<?php

namespace Entrada\Modules\Sandboxes\Acl\Assertions;

use Entrada\Modules\Sandboxes\Models\Sandbox;
use Entrada\Models\Auth\User;
use Entrada\Models\Auth\UserAccess;
use Entrada\Support\Acl\Assertion;


class SandboxOwnerAssertion extends Assertion
{

    /**
     * Verify the user's permission on the resource
     *
     * @param User       $user
     * @param int        $resourceId
     * @param UserAccess $context
     * @param string     $privilege
     *
     * @return boolean
     */
    protected function isUserAllowed($user, $resourceId, $context = null, $privilege = null): bool
    {
        $sandbox = Sandbox::find($resourceId);

        if (!$sandbox) {
            return false;
        }

        return $sandbox->created_by === $user->id;
    }

}

Creating Policies

Laravel provides a resource authorization system which it calls Gates. Due to the dynamic nature of gates, we are able to neatly map gate policy methods directly to Elentra's Zend ACL permission system. We begin by extending the base Policy class provided in the support directory. This class has a protected property that contains a reference to the Entrada_ACL class.

The Acl facade provided in the support directory can also be used and is preferable to directly accessing the global $ENTRADA_ACL instance.

For a standard API resource we will create public methods for each CRUD operation.

The names of the methods defined in policies are arbitrary as Laravel will use the magic __call method to map gates to policy method calls.

In each method we call the amIAllowed method on the policy's ACL instance, passing to it a new SandboxResource instance and the permission being requested. Because the policy in this example is only used when authorizing API requests, we are not required to pass along the $user parameter. The third parameter of amIAllowed specifies whether or not the rule's assertions should be applied. When performing general authorization without a specific resource instance, we can simply pass false to disable assertion checks.

Methods that check whether a user is allowed to create a new resource do not receive a second parameter with the resource instance.

<?php

namespace Entrada\Modules\Sandboxes\Policies;

use Entrada\Modules\Sandboxes\Acl\Resources\SandboxResource;
use Entrada\Modules\Sandboxes\Models\Sandbox;
use Entrada\Models\Auth\User;
use Entrada\Support\Acl\Policy;


class SandboxPolicy extends Policy
{

    /**
     * Determine whether the user can create a model instance.
     *
     * @param User $user
     * @return boolean
     */
    public function create(?User $user)
    {
        return $this->acl->amIAllowed(new SandboxResource(), 'create', false);
    }

    /**
     * Determine whether the user can view the model instance.
     *
     * @param User    $user
     * @param Sandbox $sandbox
     *
     * @return bool
     */
    public function read(?User $user, Sandbox $sandbox = null)
    {
        if ($sandbox === null) {
            return $this->acl->amIAllowed(new SandboxResource(), 'read', false);
        }

        return $this->acl->amIAllowed(new SandboxResource($sandbox->id), 'read');
    }

    /**
     * Determine whether the user can update the model instance.
     *
     * @param User    $user
     * @param Sandbox $sandbox
     *
     * @return bool
     */
    public function update(?User $user, Sandbox $sandbox = null)
    {
        if ($sandbox === null) {
            return $this->acl->amIAllowed(new SandboxResource(), 'update', false);
        }

        return $this->acl->amIAllowed(new SandboxResource($sandbox->id), 'update');
    }

    /**
     * Determine whether the user can delete the model instance.
     *
     * @param User    $user
     * @param Sandbox $sandbox
     *
     * @return bool
     */
    public function delete(?User $user, Sandbox $sandbox = null)
    {
        if ($sandbox === null) {
            return $this->acl->amIAllowed(new SandboxResource(), 'delete', false);
        }

        return $this->acl->amIAllowed(new SandboxResource($sandbox->id), 'delete');
    }

}

Registering Policies

Laravel provides many different ways for resources to be authorized depending on your specific use case. In this example we'll be focusing on using Service Providers, the most commonly used method for registering policies in Elentra. We begin by creating a new class in our Sandbox module's Providers directory that extends the Illuminate\Foundation\Support\Providers\AuthServiceProvider base class. This class provides a protected array that maps models to policies, here we have mapped our Sandbox model to our SandboxPolicy . Create a new method called boot that calls the base class' registerPolicies method.

A service provider'sbootmethod will be called automatically by Laravel once the application has finished registering all of its service providers.

<?php

namespace Entrada\Modules\Sandboxes\Providers;

use Entrada\Modules\Sandboxes\Models\Sandbox;
use Entrada\Modules\Sandboxes\Policies\SandboxPolicy;
use Caffeinated\Modules\Support\AuthServiceProvider as ServiceProvider;


class AuthServiceProvider extends ServiceProvider
{

    /**
     * @var array Registered Gate policies
     */
    protected $policies = [
        Sandbox::class => SandboxPolicy::class
    ];

    /**
     * Register bindings in the container.
     *
     * @return void
     */
    public function register(): void
    {
        $this->registerPolicies();
    }

}

Authorizing Requests

The preferred method for authorizing API requests is to call the controller's authorize helper in each controller's request handler. There are times, however,where a more general or more fine-grained approach is acceptable. The authorize helper requires a string for its first parmeter that corresponds to the exact method name defined in our SandboxPolicy above. The second parameter accepts either a model instance or class name and the optional third parameter is an array of any additional arguments you would like to pass to the policy method. The authorize helper returns true if the current user is allowed to access the resource or throws an exception if they cannot. If an unauthorized exception is thrown, a status code of 403 will automatically be returned in the response.

In our controller's index and store methods we pass the Sandbox class instead of a sandbox instance because we do not have a specific ID in these contexts to check against.

<?php

namespace Entrada\Modules\Sandboxes\Http\Controllers;

use Entrada\Http\Controllers\Controller;
use Entrada\Models\Auth\User;
use Entrada\Modules\Sandboxes\Models\Sandbox;
use Entrada\Modules\Sandboxes\Services\SandboxService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;


class SandboxController extends Controller
{

    /**
     * @var SandboxService SandboxService instance injected via constructor injection
     */
    protected $sandboxService;

    /**
     *
     * @param SandboxService $sandboxService
     */
    public function __construct(SandboxService $sandboxService)
    {
        $this->sandboxService = $sandboxService;
    }

    /**
     * Retrieve a listing of the Sandbox resource
     *
     * @param Request $request
     *
     * @return Response
     * @throws
     */
    public function index(Request $request): Response
    {
        $this->authorize('read', Sandbox::class);

        return response(Sandbox::all(), Response::HTTP_OK);
    }

    /**
     * Retrieve the specified Sandbox resource
     *
     * @param Request $request
     * @param Sandbox $sandbox
     *
     * @return Response
     * @throws
     */
    public function show(Request $request, Sandbox $sandbox): Response
    {
        $this->authorize('read', $sandbox);

        return response($sandbox, Response::HTTP_OK);
    }

    /**
     * Create a new Sandbox resource
     *
     * @param Request $request
     * @param Sandbox $sandbox
     *
     * @return Response
     * @throws
     */
    public function store(Request $request, Sandbox $sandbox): Response
    {
        $this->authorize('create', Sandbox::class);

        $sandbox->fill($request->all());
        $sandbox->validate();

        if (!$sandbox->save()) {
            abort(Response::HTTP_INTERNAL_SERVER_ERROR, __('An error occurred when creating the sandbox'));
        }

        return response($sandbox, Response::HTTP_CREATED);
    }

    /**
     * Update an existing Sandbox resource
     *
     * @param Request $request
     * @param Sandbox $sandbox
     *
     * @return Response
     * @throws
     */
    public function update(Request $request, Sandbox $sandbox): Response
    {
        $this->authorize('update', $sandbox);

        $sandbox->fill($request->all());
        $sandbox->validate();

        if (!$sandbox->save()) {
            abort(Response::HTTP_INTERNAL_SERVER_ERROR, __('An error occurred when updating the sandbox'));
        }

        return response($sandbox, Response::HTTP_OK);
    }

    /**
     * Delete a Sandbox resource
     *
     * @param Request $request
     * @param Sandbox $sandbox
     *
     * @return Response
     * @throws
     */
    public function destroy(Request $request, Sandbox $sandbox): Response
    {
        $this->authorize('delete', $sandbox);

        if (!$sandbox->delete()) {
            abort(Response::HTTP_INTERNAL_SERVER_ERROR, __('An error occurred when deleting the sandbox'));
        }

        return response($sandbox, Response::HTTP_OK);
    }

}

Checking Permissions

Authorization can also be performed inline using the auth user instance. The User::can method takes the same parameters as the controller authorize helper. However it does not throw an exception and will only return either true or false depending on whether a user can access the resource.

$user = Auth::user();

if ($user->can('read', $sandbox)) {
    // do something
} else {
    // do something else
}

Additional Resources

Laravel Authorization Documentation

Last updated