So my goal was to only allow the logged in user to view or make changes to data that belongs to him. How do I know which data belongs to him? In the model LanguagePack
I save his user id. Any other data like the WordList
model has a relation to language pack.
A user accesses the data via an url like this:
/languagepack/wordlist/1
So I don't want him to be able to change the id to 2 if that language pack wasn't created by him and then see and edit that data.
To do this I created a policy class that looks like this:
<?php
namespace App\Policies;
use App\Models\LanguagePack;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class LanguagePackPolicy
{
use HandlesAuthorization;
public function view(User $user, LanguagePack $languagePack)
{
return $user->id === $languagePack->userid;
}
public function update(User $user, LanguagePack $languagePack)
{
return $user->id === $languagePack->userid;
}
public function delete(User $user, LanguagePack $languagePack)
{
return $user->id === $languagePack->user_id;
}
}
Then I created a middleware for getting the language pack model from the route and running the policy check by authorizing actions. If the action is allowed, it will continue the request, otherwise it throws a 403 error and aborts the request.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class AuthorizeLanguagePack
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
$languagePack = $request->route('languagePack');
if ($languagePack) {
$user = $request->user();
if ($user->can('view', $languagePack) ||
$user->can('update', $languagePack) ||
$user->can('delete', $languagePack)) {
return $next($request);
}
abort(403, 'Unauthorized');
}
return $next($request);
}
}
The middleware has to be registered in the file src/app/Http/Kernel.php
like this:
protected $routeMiddleware = [
//add to the list of route middlewares
'authorize.languagepack' => \App\Http\Middleware\AuthorizeLanguagePack::class,
];
And finally we add the middleware key authorize.languagepack
to the routes in web.php
:
Route::middleware(['auth', 'authorize.languagepack'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index'])->name('home');
Route::get('languagepack/create', [LanguageInfoController::class, 'create']);
Route::get('languagepack/edit/{languagePack}', [LanguageInfoController::class, 'edit']);
Route::get('languagepack/wordlist/{languagePack}', [WordlistController::class, 'edit']);
})
There are probably other ways to achieve the same result. Let me know in the comments if you know of a better way...
Top comments (2)
Love your article, it's a great start and it could be improved we get rid of the custom middleware and first authorize directly into the controller like in the example below
Alternatively in the controller's constructor, assuming your routes are "resourceful"
Also Laravel already has the
can
middleware built in by default so you can easily do thisI hope this helps, also check out this article on an opinionated way to implement Laravel's authorization
@slimgee Thanks for your comment. In my case it made sense to use a middleware as I don't want to add
$this->authorizeResource('languagePack');
to each controller class or use the built-in middleware for each route (I have many more routes in than I listed in the example). Although I guess if I had created a base controller class, I could have just added it once... There are often many solutions to the same problem and I'm glad for your comment as it will help others who stumble accross it to see what other options exist.