Hello there, how are you, how is it going? This series we are going to explore Laravel policies and how we can fit them together with roles and permissions. In today’s article we are going to only focus on Laravel policies.
Background Story
Let’s say there was this online shop where a user has a bank account. There was this route:
Route::get('/bank-accounts/{bankAccount}', [BankAccountController::class, 'show'])->middleware('auth');
This route returns information about a user’s bank account.
Here is the controller:
getAttributes();
}
}
Model:
And migration:
id();
$table->unsignedBigInteger('user_id')->index();
$table->string('iban')->index();
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('bank_accounts');
}
};
Now since we have the registered ->middleware(‘auth’) on the route we want to permit only logged in users to see their bank accounts.
Therefore, I created 2 bank account in the database:
Lets login with the first user and test the route. This is the result:
Perfect, our user is logged in but what if we tried to access another bank account, the one with id 2 which doesn’t belong to the first user?
Oops, that is something which should not be allowed! Imagine someone seeing your bank account from amazon, ebay, ali express or some other online shop. We need to fix this and that’s where Laravel policies come to the rescue!
Creating A Standard Policy
You remember that in the begging of this article I said something like this:
– “… a more generic approach to Laravel policies”
And you might be wondering “Which generic approach???”.
Let me explain, when we talk about policies, we talk about resource authorization. In other words we need to authorize that the user can or cannot execute an operation on a given resource. An operation could be e.g. all the standard CRUD operations.
Out of the box Laravel can authorize these operations:
With “… a more generic approach to Laravel policies” it is meant to have 1 standard policy which authorizes those operations versus having 1 policy / model. So let’s create this standard policy. We can do this by running this artisan command.
php artisan make:policy StandardPolicy –model=BankAccount
Most of the time, a policy only needs to authorize that the resource belongs in some way to the authenticated user. In our case above we need to authorize that the user 1 can access only bank accounts belonging to him.
How Resources Relate To The User
There are 2 main cases which we have to study first: directly related resources and polymorphic related resources.
Case 1 – Directly Related Resources
If a resource is directly related to the user through an user_id column e.g. our bank_accounts table, we need to see if the bank account . user_id matches the authenticated user id.
Case 2 – Polymorphic Related Resources
In cases where a resource belongs to the user through a polymorphic relationship, we need to search deeper using the model_type and model_id columns until we find a resource which contains a user_id column. From here, enters case 1 and we know what is to be done.
Writing The Code
When implementing the above theory this is the result:
authorize($user, $model);
}
/**
* @param User $user
* @param Model $model
* @return Response
* @throws Exception
*/
private function authorize(User $user, Model $model): Response
{
switch (true) {
case (Schema::hasColumn($model->getTable(), $this->userIdColumn)):
$condition = $this->relatesTo($user, $model);
break;
case (Schema::hasColumns($model->getTable(), $this->morphColumns)):
$condition = $this->morphsTo($user, $model);
break;
default:
throw new Exception('User not authorized.');
}
return $condition ? $this->allow() : $this->deny();
}
/**
* @param User $user
* @param Model $model
* @return bool
*/
private function relatesTo(User $user, Model $model): bool
{
$relatedUserId = $model->{$this->userIdColumn};
return is_int($relatedUserId) && $user->id === $relatedUserId;
}
/**
* @throws Exception
*/
private function morphsTo(User $user, Model $model): bool
{
if ($model->model_type === User::class) {
return $user->id === $model->model_id;
}
$morphedModel = $model->model;
if ($morphedModel instanceof Model) {
return $this->authorize($user, $morphedModel)->allowed();
}
throw new Exception('Expected instance of Model but something else was given!');
}
}
Replacing The Standard Policy Discovery
We have to replace the standard policy auto discovery with the standard policy. This can be simply achieved by overriding guessPolicyNamesUsing from the IlluminateSupportFacadesGate class in the AuthServiceProvider.
*/
protected $policies = [
// 'App\Models\Model' => 'App\Policies\ModelPolicy',
];
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
// if policy is registered in $policies property, that will take precedence over discovery
Gate::guessPolicyNamesUsing(function ($modelClass) {
return StandardPolicy::class;
});
}
}
Registering Policy To Route
Now, there is one last step remaining. We need to register the required authorization on our route using the can middleware. First, I created a PolicyMethod.php config class containing all the policies methods:
And then registered the ‘view’ ability on the route:
And create the test routes:
Route::prefix('/posts')->group(function () {
Route::get('/form', [PostController::class, 'form']);
Route::post('/create', [PostController::class, 'create']);
Route::get('/', [PostController::class, 'index']);
Route::get('/{post}', [PostController::class, 'show']);
});
Test of Truth
Let’s try to access the bank account which didn’t belong to the first user:
Brilliant! If we accessed the correct bank account, we should be able to access it:
Great! Let’s also create a polymorphic resource. We will call it payments.
Payments can relate to a bank account or to a new resource called virtual bank account. Let’s create this entities:
class VirtualBankAccount extends Model
{
use HasFactory, SoftDeletes;
protected $guarded = ['id'];
}
Schema::create('virtual_bank_accounts', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id')->index();
$table->string('code');
$table->timestamp('activated_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamps();
$table->softDeletes();
})
class Payment extends Model
{
use HasFactory, SoftDeletes;
protected $guarded = ['id'];
public function model(): MorphTo
{
return $this->morphTo();
}
}
Schema::create('payments', function (Blueprint $table) {
$table->id();
$table->morphs('model');
$table->unsignedBigInteger('product_id')->index();
$table->timestamps();
$table->softDeletes();
});
Good, now let’s create a PaymentController and a route:
getAttributes();
}
}
Route::get('/payments/{payment}', [PaymentController::class, 'show'])
->middleware('auth')
->can(PolicyMethod::VIEW, 'payment');
It’s time to test the polymorphic relation access. First, I created a virtual bank account for each of our users:
Let’s register some payments:
We know that our first user with id 1 owns the bank account and the virtual bank account with the id 1. Our policy should not allow him to view the payment belonging to the second user. If we test our theory:
We have confirmation! The standard policy is working as expected. Only resources belonging to the user can be accessed.
Wrap Up
Laravel policies are a secure and powerful way to authorize users to access resources from the database. Feel free to play around and adjust the code as you like. In our next blog article we are going to see how we can fit our standard policy with a simple roles & permissions system.
Until then stay tuned, eat a healthy breakfast, enjoy your day and as always, happy coding!
Useful Links:
We have helped 20+ companies in industries like Finance, Transportation, Health, Tourism, Events, Education, Sports.