DEV Community

fractalbit
fractalbit

Posted on

How to create a custom Policy and enforce it in a BREAD (Voyager)

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:

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

Open Workstation.php and add the following method

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();
}
Enter fullscreen mode Exit fullscreen mode

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

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

Since we want the users to not see the other records at all we can easily do so by creating a Scope 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

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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

Important: This is not strictly needed if we are using a Scope (see 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.

php artisan make:policy WorkstationPolicy
Enter fullscreen mode Exit fullscreen mode

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

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');
    }
}
Enter fullscreen mode Exit fullscreen mode

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. 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 and before "@foreach ($dataTypeContent as $data)" add the following code:

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

@foreach($dataTypeContent as $data)
Enter fullscreen mode Exit fullscreen mode

The recommended way is to use the Scope method 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

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, 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!

Discussion (1)

Collapse
nesvetaevlogema profile image
NesvetaevLogema

Thank you, this is exactly what I needed!