How to create a custom Policy and enforce it in a BREAD (Voyager)
Tags: laravel, voyager, policy, override
Published: 2020-07-18 13:50:40

Let's say we want to limit access to a BREAD based on who created it. More specifically users should be able to create new records but only edit/delete and browse their own. Admins should have full control to all the records regardless of who created it.

Along the way, you will learn:

- [How to Create a custom Policy and enforce it to a particular BREAD](#policy)
- [How to override the save method of a Model](#override-model)
- [How to scope results](#scope-results)
- [How to override Voyager's Views](#override-view)

I assume you have basic familiarity with the Voyager admin (ex. how to create a BREAD for a db table) and with basic concepts of OOP PHP and laravel.

So let's get started. For our example, let's say we have a Workstation Model to store data for their specs and each company (identified with a user account) should be able to add/edit/view only their own workstations. We have also created the respective BREAD for our table from the voyager admin.

First we need to add an **owner_id** field to our table and exclude it from the BREAD (uncheck all or at least Edit, Add and Delete). **The field should be populated automatically when creating a new record**, so the users should not be able change it. To do this we will override the save method of our Model.

## Override the save method of our Model <a name="override-model"></a>

Open Workstation.php and add the following method

```php
public function save(array $options = [])
{
    // If no owner has been assigned,
    // assign the current user's id as the owner of the workstation
    if (!$this->owner_id && Auth::user()) {
        $this->owner_id = Auth::user()->getKey();
    }

    return parent::save();
}
```

Now everytime a workstation is saved or updated, we will first check if the owner_id is null. If it is, it will be populated with the id of the current logged in user. If it already has a value, it will not be changed. That means that the owner_id is only saved once, when we create a record, and will not be changed if another user (ex. admin) updates the record (if this happened the original owner would loose access to his record).

## Important update <a name="important-update"></a>

Since publishing the article, I posted the link to Voyager's slack channel looking for suggestions. As it turns out, **there is a much simpler and cleaner way** to achieve what we want without using a Policy and overriding the browse View. The only thing needed is to create a new Scope for our Model and use it in the respective BREAD. Let's see how:

## Create a Model Scope to limit access <a name="scope-results"></a>

Since we want the users to not see the other records at all we can easily do so by creating a [Scope](https://voyager-docs.devdojo.com/bread/introduction#scope-browse-results) for our Model. To create a new Scope we edit our Model (ex. Workstation.php) and add a scopeScopeName method. This method filters the query based on the conditions we want. The end result should look like this:

```php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;

class Workstation extends Model
{
    public function scopeCurrentUser($query)
    {
        return Auth::user()->hasRole('admin') ? $query : $query->where('owner_id', Auth::user()->id);
    }

    public function save(array $options = [])
    {
        // If no owner has been assigned, assign the current user's id as the owner of the workstation
        if (!$this->owner_id && Auth::user()) {
            $this->owner_id = Auth::user()->getKey();
        }

        return parent::save();
    }
}
```

Then we edit the respective BREAD and in the "**Scope**" dropdown field we select "**currentUser**". We save the BREAD and we are done!

Now users will only see their own records and admins all the records. If a user tries to "hack" and visit an edit or view link for a record he doesn't own (ex. "admin/workstations/2/edit") he will be greeted with a "404 - Not found" page.

## Create a new Policy <a name="policy"></a>

**Important**: This is not strictly needed if we are using a Scope (see [important update](#important-update)). But in case you want to learn how to use a custom Policy i will leave it in the article (it is in the article's title after all). You may also want to add another layer of security.

To create a new Policy run the following in the terminal.

```terminal
php artisan make:policy WorkstationPolicy
```

This will create a WorkstationPolicy.php file in app\Policies that we need to edit. After the modifications the Policy should look like this:

```php
<?php

namespace App\Policies;

use TCG\Voyager\Contracts\User;
use App\Workstation;
use Illuminate\Auth\Access\HandlesAuthorization;
use TCG\Voyager\Policies\BasePolicy;

class WorkstationPolicy extends BasePolicy
{
    use HandlesAuthorization;

    /**
     * Create a new policy instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    // We can override all the BREAD (browse, read, edit, add and delete) actions here if we need to

    public function edit(User $user, $pc)
    {
        return $pc->owner_id === $user->id || $user->hasRole('admin');
    }

    public function delete(User $user, $pc)
    {
        return $pc->owner_id === $user->id || $user->hasRole('admin');
    }

    public function read(User $user, $pc)
    {
        return $pc->owner_id === $user->id || $user->hasRole('admin');
    }
}
```

**It is important that the new Policy extends the BasePolicy from TCG\Voyager\Policies\BasePolicy**. Otherwise we will have to define every policy action from scratch. _By extending the BasePolicy we can override only the actions we want_.

Each action we override accepts two parameters, the **current logged in user** and an **instance of the Model** we want the policy to be enforced (in this case the Workstation model, that we named **pc** for short). **We should do the checks we want and then return true or false (allow the action or not).**

For our requirements, **if the owner_id from the Model matches the id of the logged-in user OR if the user is an admin** we will return **_true_** (otherwise, if none of the conditions are met, false is returned).

Now we should go to the BREAD of our Model and in the "**Policy Name**" field enter our new policy: "**App\Policies\WorkstationPolicy**" (don't forget to save the BREAD). With this in place, if you login to your voyager admin with another, non-admin user, and browse the workstations, you will not see the edit or delete buttons for the records they have not themselves created.

## Override the browse View for our Model

**Important**: This is not needed if we are using a Scope. See [important update](#important-update). If you override the browse View **you should definetely enforce the Policy** above to block users directly visiting edit or view URLs.

To limit browsing records [override the browse view](#override-view) and before "@foreach($dataTypeContent as $data)" add the following code:

```php
@php
    if(!Auth::user()->hasRole('admin')) $dataTypeContent = $dataTypeContent->where('owner_id','=', Auth::user()->id)
@endphp

@foreach($dataTypeContent as $data)
```

The recommended way is to use the [Scope method](#scope-results) above. I am leaving the alternative method because you may find useful how to override a Voyager View for something else.

## How to override a Voyager View <a name="override-view"></a>

Overriding views is very easy. First we copy the view we want (ex. _browse.blade.php_) from _"\vendor\tcg\voyager\resources\views\bread"_. Then we create the following folder structure _"\resources\views\vendor\voyager\\**workstations**"_ and paste the view there. Now we can do any modification we need for this BREAD's view and **voyager will first look if our version exists before using it's own**. You should be careful so that the last **folder name matches your Model's name with lowercase letters and in plural form** (or the "URL Slug" you have entered for that BREAD).

Finally you may also want to check the article [Using Policies in Laravel Voyager](https://webman.io/blog/post/using-policies-in-laravel-voyager), it was of great help for me.

Do you have something to add? Maybe a better way to accomplish the above? Maybe you spotted a mistake? Please tell me in the comments, thank you!