Create generic route-based authorization in Laravel - laravel

I'm coming from conventional PHP background and trying to create my first big project in Laravel.
I usually user User/Role/Permission to manage user permissions in my applications. It works like follows:
User has many Roles
Role has many Permissions
to make things simple, I actually used the page names as permissions, so that I check the current page name against user permissions.
That was all easy in PHP, now I am trying to implement a similar approach in Laravel. I have User, Role, Permission models, and I check if user has permission using a method in User model as follows (inspired from a Laracasts tutorial):
public function permissions()
{
return $this->roles->map->permissions->flatten()->pluck('name')->unique();
}
And in my AuthServiceProvider I added the following code:
Gate::before(function ($user, $permission){
return $user->permissions()->contains($permission);
});
So if I add some permission (for example 'add_user') to the user, I can simply do the following in the route, and it works just fine:
Route::get('/test', function () {
return 'You are authorized';
})->name('add_user')->middleware('can:add_user');
Now since I have a lot of pages, I wouldn't like to pass specific permission name to the middleware, rather find a better and more generic way.
The only way I could come up with is to use the permission name same as the route name, and create a new middleware to take care of authorization.
So In my solution I added the following middleware class:
class BeforeMiddleware
{
public function handle($request, Closure $next)
{
$route_name = $request->route()->getName();
if(!Auth::user()->permissions()->contains($route_name)) {
throw new \Exception('Not Authorized');
}
return $next($request);
}
}
Added it to Kernel.php:
protected $routeMiddleware = [
'before' => \App\Http\Middleware\BeforeMiddleware::class,
...
];
And finally changed the route to be as follows:
Route::middleware(['before'])->group(function () {
Route::get('/test', function () {
return 'You are authorized';
})->name('add_user');
});
This way I don't actually have to pass the permission name when I check the permission, and directly get it from the route name.
I have many questions about my solution: is it really a good approach? Does it have any drawbacks? Is there a better approach?
Also I preferred to use AuthServiceProvider instead of the new middleware, but I couldn't retrieve the route name from ServiceProvider scope. Can I somehow use AuthServiceProvider for a similar case?
Sorry if I made the post somehow long, but I needed to be as clear as I could.

Related

Laravel - Controller dependency is injected before middleware is executed

So I created a middleware to limit the data a connected user has access to by adding global scopes depending on some informations:
public function handle(Request $request, Closure $next)
{
if (auth()->user()?->organization_id) {
User::addGlobalScope(new OrganizationScope(auth()->user()->organization));
}
return $next($request);
}
The middleware is added to the 'auth.group' middleware group in Kernel.php which is used in web.php:
Route::middleware(['auth.group'])->group(function () {
Route::resource('users', UserController::class);
});
Then in the controller, I would expect a user to get a 404 when trying to see a page of a user he has no rights to. But the $user is retrieved before the middleware applies the global scope!
public function show(User $user, Request $request) {
// dd($user); // <= This actually contains the User model! It shouldn't, of course.
// dd(User::find($user->id)); // <= null, as it should!
}
So, the dependency is apparently calculated before the middleware is applied. If I'm trying to move the middleware into the 'web' group in Kernel.php it's the same. And in the main $middleware array, the authenticated user's data is not available yet.
I found this discussion that seems to be on topic : https://github.com/laravel/framework/issues/44177 but the possible solutions (and Taylor's PR) seems to point to a solution in the controller itself. Not what I'm trying to do, or I can't see how to adapt it.
Before that I was applying the global scopes at the Model level, in the booted function (as shown in the docs). But I had lots of issues with that - namely, accessing a relationship from there to check what is allowed or not is problematic, as the relationship call will look for something in the Model itself, and said model is not ready (that's the point of the booted method, right...). For example, checking a relationship of the connected user on the User model has to be done with a direct query to the db, that will be ran every time the Model is called... Not good.
Anyway, I like the middleware approach as it is a clean way to deal with rights as well, I think. Any recommandation?
Not a recommendation, just my opinion.
This issue is just because of that Laravel allow you add middleware in controller constructor, and that's why it calculate before midddleware in your case.
I agree that middleware is a clean way to deal with auth, but i also think that you are not completely doing auth in your middleware, for example if you create a new route will you need to add something auth action into your new controller or just add auth middleware to route?
If does needs add something to controller, that means your auth middleware is just put some permissions info into global scope and you are doing the auth in controller which i think it's not right.
Controller should be only control the view logic, and you should do full auth in your auth middleware, once the request passed into your controller function that means user passed your auth.
For some example, if you auth permissions like below, you can just add auth middleware to new route without any action in your controller when you trying to create new route.
public function handle(Request $request, Closure $next)
{
if (auth()->user()->canView($request->route())) { // you should do full auth, not just add informations.
return $next($request);
}
else
abort(404);
}

Laravel permission on same resource for multiple roles (Spatie)

I need to grant access to users with different roles to actions of one resource.
I tried the following but no luck in web.php routes file:
Route::resource('trampas', 'TrampaController')->middleware('role:Administrador|Supervisor'); //Access to all actions
Route::resource('trampas', 'TrampaController')->middleware('role:Monitoreador|Coordinador')->only('index', 'show');
But when i declare the 2nd line the first in annulled.
Same thing happens when it's declared in the controller's construct method:
$this->middleware('role:Administrador');
$this->middleware('role:Coordinador')->only('index','show');
Last line permission prevails.
Any ideas?
Middleware must all pass before the request is processed. If you want index and show to pass one of the roles you need to explicitly put all roles in the parameters.
In your controller:
class TampaController extends Controller {
public function __construct() {
$this->middleware('role:Administrador|Supervisor');
$this->middleware('role:Administrador|Supervisor|Monitoreador|Coordinador')->only('index', 'show');
}
// ...
}
I can't get why this is working:
public function __construct()
{
$this->middleware('role:Administrador|Supervisor')->except('index', 'show');
$this->middleware('role:Administrador|Supervisor|Monitoreador|Coordinador');
}
when what it seems to do is the opposite of what i need.
I need the the roles Administrador and Supervisor can access all actions and roles Monitoreador y Coordinador can only access to index and show actions. But the way it works seems to declare that Administrador and Supervisor can access all except for index and show actions.
Why could this be happening?

How to create single endpoint for authenticated or non-authenticated User in laravel?

I need a single endpoint where I can check if User authenticated then can return some user-related data else(if not-authenticated) some basic information that I want.
I tried by checking user having a token or not in the api.php file. but it is not working.
The example below is basically generated by following the official documentation.
//file: ./routes/web.php
use Illuminate\Support\Facades\Auth;
Route::get('profile', function () {
if (Auth::check()) {
return Auth::user();
} else {
return ['foo' => 'bar'];
}
});
Have in mind that this is extremely simplified example.
In a real world case use probably you would have a route pointing to a controller method where you can auto-inject the Illuminate\Auth\AuthManager.
Also you wouldn't be returning the whole user object, but rather transforming the response to your needs.

Controller constructor to check Auth middleware for two different guards

I have a dashboard view that shows certain contain depending on which user is viewing, whether it be an admin or just a regular user.
I can get my admins onto that page, but regular users aren't able to currently because of my middleware guard.
class DashboardController extends Controller {
public function __construct()
{
$this->middleware('auth:admin');
}
public function index()
{
return view('dashboard.index');
}
}
The following code checks on each DashboardController call for auth:admins, but I want regular users to access this too, is there a way to check the auth middleware twice like so?
$this->middleware(['auth:admin','auth']);
So ideally it will check if you're an admin or just a regular auth user.
Also on my view page, when accessing properties of an admin I'm using:
{{ Auth::user()->admin_username }}
Is this normal? I have an admin Model but I'm still accessing it via Auth::user() which feels strange to me, shouldn't it be Auth::admin()->admin_username
Accessing a particular page for users with differing roles is more suited for laravels gates and policy authorization mechanisms.
https://laravel.com/docs/5.5/authorization#writing-gates
These allow you to write fine tuned rules for each use case you have. Simple gates can be defined as closures within your application AuthServiceProvider. For example:
public function boot()
{
$this->registerPolicies();
Gate::define('access-dashboard', function ($user, $post) {
return auth()->check() && (auth()->user()->hasRole('admin') || auth()->user()->hasRole('regular'));
});
}
Then you can use the gate facade wherever necessary, for instance a controller method or constructor.
if (Gate::allows('access-dashboard', $model)) {
// The current user can access dashboard, load their data
}
Alternatively use the can or cant helpers on the user model directly.
if (auth()->user()->can('access-dashboard')) {
//
}
Of course, you can achieve similar via middleware, the advantage of using the above is you can authorize actions at specific points in your code as well as reusability.
As for for last question, as you have it written is correct.
{{ Auth::user()->admin_username }}
Auth::user() or auth()->user() simply returns the currently authenticated user, regardless of their role.
Policies will never work without auth middleware

How to catch any link that came from upload/ in laravel 5?

im new in laravel 5.2, I just want to ask how you can catch a link that came from uploads like: http://sitename.com/uploads/59128.txt? I want to redirect them to login page if they tried to access any of route or link that came from uploads/{any filename}.
Yes you can achieve by protecting your route with auth middleware,
make a small FileController
class FileController extends Controller {
public function __construct()
{
$this->middleware('auth');
}
public function getFile($filename)
{
return response()->download(storage_path($filename), null, [], null);
}
}
and then in routes.php
Route::get('file/{filename}', 'FileController#getFile')->where('filename', '^[^/]+$');
And that's it. Now, your authenticated users can download files from storage folder (but not its subfolders) by calling http://yoursite.com/file/secret.jpg. Add you can use this URL in src attribute of an image tag.
answer's original source!
#xerwudjohn simple you can't.
When this file is in the public folder, everyone can access it whitout being logged in.
One method I tried for some minutes, create a new route:
Route::group(['middleware' => ['web', 'auth']], function () {
Route::get('/download/{id}', 'DownloadController#showFile');
});
create the function showFile in the DonwloadController
public function showFile($id)
{
return redirect('/image/'.$id.'.txt');
}
or use a Model to read uniqueIds out of any table and get the realfile name.
Cheers

Resources