Laravel

Laravel 5.5.14 - PHPUnit 6 - Policy - Action Not Authorized

Issue

After upgrading to Laravel 5.5.14 from 5.4, I received errors on two tests that “This action is unauthorized”. Both tests were delete commands, and both employ the use of policies.

;TLDR

Make sure that you have a function named for the action you are authorizing against. In this case, I was calling:

$this->authorize('delete',$contactType);

did not have a “Delete” function created within the ContactType Policy.

Background

In this application, I am only allowing Site Administrators to remove the Contact Types. Therefore, in the “Before” function, I am checking if the user is a Site Administrator. If he is, the allow the action to occur. If he is NOT, then proceed to the actual method and review the authorization there:

public function before($user, $ability)
{
if (! $user->getSelectedRole()) {
return false;
}

if ($user->isSiteAdministrator()) {
return true;
}
}

did not have a “Delete” function created, as no other user type is allowed to perform this action…and if the function didn’t exist, it returned false. Cool.

Solution

In 5.5, I guess a change was made to check that the requested function exists first, then process the “before” method. I’m not 100% on this as the issue, but I simply corrected the error by creating a “delete” method within the policy:

/**
 * Determine whether the user can delete contact types.
 * @param\App\User $user
 * @return mixed
 */
public function delete(User $user)
{
return false;
}

Laravel - JSON - Failed Authorization Returns HTML Instead of JSON

ISSUE:

JSON requests to backend that fail middleware checks return HTML page vs JSON Object

;TLDR:

Within each middleware check created, add a JSON check for the request, and included the appropriate message as well as the status code.

DETAILS:

I am in the process of changing the CRUD operations of my template to Vue/axios instead of old school page posts to a backend controller. 

Prior to this change, I was using a combination of middleware to verify access to the controller, and individual policies for each of the commands (Create,Read,Update,Delete).  For example, I may want a certain class of user to be able to view the resources, but only administrators can update the resource.  I grant both roles the "view-{modelName}" permission, where {modelName} is the actual model name of the resource (location, company, etc).  Then I grant the admin role the permission of 'edit-model', and use the policy of the resource to validate their access.

In changing over to a json POST, I'm now expecting a JSON response - which works great for successful submissions and validation errors.  However, I was finding that in my authorization tests that verify a specific user can not access a resource, I noticed that my JSON requests were always returning the HTML "Insufficient Privileges" page instead of the JSON version.

This is because the user verification was not passing my middleware permissions check. It was working as intended, but was only setup to return the HTML response and NOT JSON.

CODE:

App/Controllers/Middleware/CheckPermission.php:

public function handle($request, Closure $next, ...$permissions)
{
    $userRole = $request->user()->getSelectedRole();

    if ($userRole !== null) {
        foreach ($permissions as $permission) {
            if ($userRole->can($permission))
            {
                return $next($request);
            }
        }
    }
    // here is the change I added
    if ($request->expectsJson())
    {
        return response()->json(['success' => false,'message' =>"Sorry, you are not authorized"], 403);
    }

    return redirect('/insufficient-privileges');
}

Laravel EagerLoading ALWAYS INCLUDE...

ISSUE:

There are certain models that have relationships that I ALWAYS want loaded.  

 

QUICK FIX: 

In the model for the item, add a protected $with variable.  See the following code:

Class MyModel extends Model {
protected $with = ['relation'];
}

In my case, I have a Location model, and the associated model is an Address model.  Locations can have multiple addresses, and MUST have at least one address.  In my case, the code would look like the following:

Class Location extends Model {
protected $with = ['addresses'];
}

Now, whenever I return a location object, it will automatically include the associated addresses with it.

Caveat?

This definitely isn't the solution for EVERY associated relationship - only the ones that are likely to be needed 80% or more of the time.  Perhaps even a little less.  However, the solution that Eager Loading provides outweighs most of the concerns that might come up from loading a small amount of resources every time.

Laravel Tests, Model Arrays and Appended Attributes

Problem:

After creating "appended" columns in a Laravel model, tests that converted that model to an array (or JSON) began to fail as they were trying to insert the appended column to the database.

UGH - Quick Fix Please:

Before converting your model toJSON or toArray, use the makeHidden('appended_column_name') option to hide any specific appended columns.

OK Keep Talking.

In my template, I have three models that have images associated with them.  Instead of hard coding the route for the image, I have employed the use of an appended attribute.  This is one that is created on the model that is in addition to the columns within the database. You can read more about appended attributes here.

This means instead of coding "/path/to/image/image.png", I can just call model->image_url. BOSS.  Ok.

In my Unit Tests, I have created several helper functions that make live easier overall when creating the tests themselves.  For example, in my "CompanyTest", I have a function that creates a company, and I call this from other functions where I need a default company created.

1. The make is a helper function that just returns a factory created model of the class passed in.2. When posting to /admin/companies, I pass in the $company->toArray, which automatically passes in all the values needed to save it to the database.

1. The make is a helper function that just returns a factory created model of the class passed in.

2. When posting to /admin/companies, I pass in the $company->toArray, which automatically passes in all the values needed to save it to the database.

After making my "appends" change, tests that used this helper function no longer passed.  Specifically, I received the following message:

appends2.png

When converting a model toArray() or toJson(), any appended values are also included.  This means that when I passed the array(ed?) model to the post endpoint, it tried to insert the image_url field into the database.  As that column does not exist, the insert failed.

WHAT TO DO? Hide That Attribute, Yo!

It's possible to hide specific attributes prior to converting the model to an array.  You can read more about that here.  Once I made the change, all tests passed again (whew).

append3.png