How to pass more than one gate in middleware? (Laravel) - laravel

I am creating a Learning Management System for my university final year project (only recently introduced to laravel). I have set up three different roles (admin, instructor and student). I have created two views which only the admin&instructor can access, 'user management' and 'course management'. Within each admin&instructor can create users/courses and delete as required.. a student cannot view these or has access to so that is working as desired. To do so I have created a gate 'manage-user' and then passed this into the middleware.
I have now created a calendar, which I would like all user roles to view.. again i created a gate for this.. due to my current middleware i am getting 'unauthorised access' when a student attempts to view the calendar.. is it possible to pass another gate within the middleware? I tried to do so with no success.. After many attempts of trial and error I have resulted to asking a question on here hoping i can figure this out...
i will paste my code below.. any help is appreciated.
AuthServiceProvider.php
public function boot()
{
$this->registerPolicies();
//User Management
Gate::define('manage-users', function($user){
return $user->hasAnyRoles(['admin', 'instructor']);
});
//Calendar
Gate::define('manage-calendar', function($event){
return $event->hasAnyRoles(['admin', 'instructor', 'student']);
});
web.php
Route::get('/', function () {
return view('welcome');
});
Auth::routes();
Route::get('/home', 'HomeController#index')->name('home');
Route::namespace('Admin')->prefix('admin')->name('admin.')->middleware('can:manage-users')->group(function(){
//Users
Route::resource('/users', 'UsersController', ['except' => ['show']]);
//Courses
Route::resource('/courses', 'CoursesController', ['except' => ['show']]);
Route::get('events', 'EventsController#index')->name('events.index');
Route::post('/addEvents', 'EventsController#addEvent')->name('events.add');
});
I understand that the issue lays within the gate manage-users that I have defined.. I am not sure what way to go about it protect my other routes from students &instructors...
Thanks in advance :)

The manage-users Gate will not allow a user with student role to go through the middleware, even if the manage-calendar Gate does.
I suggest you regroup the routes to apply the middleware that corresponds to each route:
Route::namespace('Admin')->prefix('admin')->name('admin.')->group(function(){
Route::middleware('can:manage-users')->group(function(){
Route::resource('/users', 'UsersController', ['except' => ['show']]);
Route::resource('/courses', 'CoursesController', ['except' => ['show']]);
});
Route::middleware('can:manage-calendar')->group(function(){
Route::get('events', 'EventsController#index')->name('events.index');
Route::post('/addEvents', 'EventsController#addEvent')->name('events.add');
});
});

Related

Laravel Route: Multiple Route Group With Prefix and Route Model Binding

Hello wonderful people of SO!
I have a problem about Laravel Route which I cannot solve.
In User.php model I use getRouteKeyName() function
public function getRouteKeyName()
{
return 'user_name';
}
And also in Post.php model
public function getRouteKeyName()
{
return 'uuid';
}
In users table, 1 have one record
|----------------------------|
| id | ... | user_name | ... |
| 1 |-----| #simple |-----|
In posts table
|------------------------------------|
| id | ... | uuid | ... |
| 1 |-----| abcd-123-efg-456 |-----|
In route (web.php)
// for post (key: uuid)
Route::group(['prefix' => '{post}'], function () {
Route::get('/', function (Post $post) {
return $post;
});
});
// for users (key: user_name)
Route::group(['prefix' => '{user}'], function () {
Route::get('/', function (User $user) {
return $user;
});
});
Then let say we visit url: www.example.test/#simple/
In debugbar, I see query:
select * from posts where uuid = '#simple' limit 1
What I have tried
[#1] I put where clause in route groups for posts and users
Route::group([
'prefix' => '{post}',
'where' => [
'post' => '^[a-zA-Z0-9-]{36}$' // I'm not Regex professional
]
], function () {
Route::get('/', function (Post $post) {
return $post;
});
});
Route::group([
'prefix' => '{user}',
'where' => [
'user' => '^(#)[a-zA-Z0-9]$' // I'm not Regex professional
]
], function () {
Route::get('/', function (User $user) {
return $user;
});
});
So let's try again visit the url: www.example.test/#simple
What i got, 404
[#2] I deleted the getRouteKeyname in both User and post model
revisit url: www.example.test/#simple, still got 404
[#3] I tried to put Route Model Binding Column Name
Route::group([
'prefix' => '{post:uuid}', // This is what I changed
], function () {
Route::get('/', function (Post $post) {
return $post;
});
});
Route::group([
'prefix' => '{user:user_name}', // This is what I changed
], function () {
Route::get('/', function (User $user) {
return $user;
});
});
Still, query result is same: > select * from posts where uuid = '#simple' limit 1
What I want to achieve
Let say we visit url: www.example.test/#simple
Fetch a user with user_name is #simple or if the user is not exist, return 404
And also same for with posts
We visit url: www.example.test/abcd-1234-efgh-5678
Fetch a post with uuid is abcd-1234-efgh-5678 or 404 if not exist
Question:
[#1] How to tell Laravel Route: that I have 2 Route groups with different Model Binding? Sorry if this question is kinda confusing, cause my english is not really good
[#2] Have I implement Best practice for route groups and route model binding in Laravel?
Thanks in advance!
What is the result you intend to obtain?
If you are doing what I think you're doing (trying to see what's inside the post), you need to return something like $post->content (replace content with the column you want to get), you may even want to make a view and make the output nicer, plus use a controller for more processing.
As for route model binding, you can refer to this, both methods, using table:column and using getRouteKeyName are fine, however, the first one doesn't change the default column, and if you use {user} for another route, it will still use the ID column, however, the second one changes the default value, if you use {user} for another route, it will use the column you specified.
Also, you should use something like user/{user:user_name} and post/{post:uuid} instead of just {user:username} and {post:uuid}, as you have said, it won't know which route you're using. The uri has to be different.
Routes are evaluated in the order you put them, meaning that the second route with {post:uuid} will override the route with {user:username} since they have the same kind of uri, that is, they both consist of 1 wildcard and nothing else. To solve this, you simply have to make their uri different by adding a static part, for example, add post/ before {post:uuid} and/or add user/ before {user:user_name} like the example below:
Route::group([
'prefix' => 'post/{post:uuid}',
], function () {
Route::get('/', function (Post $post) {
return $post;
});
});
Route::group([
'prefix' => 'user/{user:user_name}',
], function () {
Route::get('/', function (User $user) {
return $user;
});
});
To make it very clear, your 2 routes have the same uri of 1 wildcard and nothing else, thus, the last one that appears with this uri will override all the previous routes with the same uri. Meaning that all the previous routes with this same uri before this will be treated like they don't exist, and when you go to a path with the uri in the format of /[insert something here], it fits into the format of having 1 wildcard and it will only go to the last one you specified, that is, the one for posts.
Since the route for users is declared before the one for posts and they share the same uri, only the one for posts will be used. Even when you are trying to find the user, it still uses the route for posts, if no such "post" with a uuid same as the user_name you provided exists, it will still return an error even when there is indeed such user with such username.
Also, you don't need a route group if there's simply 1 route, though it would be more readable and convenient if you're going to add more routes to the group in the future.
As far as I could understand your problem, here are the changes you need to make and it will work,
routes/web.php
Route::group([
'prefix' => 'post/{post:uuid}'
], function () {
Route::get('/', function (Post $post) {
return $post;
});
});
Route::group([
'prefix' => 'user/{user:user_name}'
], function () {
Route::get('/', function (User $user) {
return $user;
});
});
Regular Expression that you use above just does filter the {argument} and check if {argument} is alphanumeric basically, in above both cases it works the same except in user_name it also allows '-'

Use different route namespace based on middleware in Laravel

I have the following code in my routes/web.php
Route::namespace('Admin')->middleware(['admin'])->group(function() {
Route::get('/posts', 'PostController#index');
});
Route::namespace('User')->middleware(['user'])->group(function() {
Route::get('/posts', 'PostController#index');
});
I wish to use the same uri "/posts" in both cases and keep the role logic (admin, user) out of the controllers, however, in this case, when I request the route "/posts" in always responds with the last one.
I can't seem to find information of what I am missing here.
use prefix for different route for admin and user
/admin/posts
Route::group(['namespace' => 'Admin','middleware=>'admin','prefix' => 'admin'],function() {
Route::get('/posts', 'PostController#index');
});
/user/posts
Route::group(['namespace' => 'User','middleware=>'user','prefix' => 'user'],function() {
Route::get('/posts', 'PostController#index');
});
You may try this one
Route::group(['prefix'=>'admin','middleware'=>'admin'],function (){
Route::get('/posts',['uses'=>' PostController#posts','as'=>'posts.index']);
});
Route::group(['prefix'=>'user','middleware'=>'user'],function (){
Route::get('/index',['uses'=>' PostController#posts','as'=>'posts.index']);
});

Resolve duplicate routes based on middleware group

In web.php I have two middleware groups for two user roles - admins and non_admins:
Route::group(['middleware' => ['auth', 'admin']], function () {
// if user is logged in AND has a role of admin...
Route::get('/', 'Admin\IndexController#index');
});
Route::group(['middleware' => ['auth', 'non_admin']], function () {
// if user is logged in AND has a role of non_admin
Route::get('/', 'NonAdmin\IndexController#index');
});
Both admin and non_admin middleware check that the role of Auth::user() is admin or non_admin respectively; if it's not, the middleware fails with abort(403). Why do I not have a single middleware? The point of this to separate the two roles, so that each has its own independent controller logic and its own views.
Problem
If I log in as an admin I get 403, if I log in as a non_admin, it works as expected. My guess: Laravel sees the two duplicate routes, and only resolves the one that is defined last (which happens to be in ['middleware' => ['auth', 'non_admin']]).
Question
How do I resolve duplicate routes but separate controller and presentation logic? Again, admin and non_admin users will visit the same route ('/') but see two different views. I also want to implement this in two different controllers.
Hmmm....
I would personally keep everything encapsulated under a common controller, and perform the necessary checks and modifications using a service class.
But if you really want to keep everything separated under two different controllers based on your roles, you could do it this way. This assumes that you're using Laravel's built-in Authorization:
routes
Route::group(['middleware' => ['auth']], function () {
Route::get('/', function(){
$isAdmin = Auth::user()->can('do-arbitrary-admin-task');
$ns = $isAdmin ? 'Admin' : 'NonAdmin';
$controller = app()->make("{$ns}\\IndexController");
return $controller->callAction('index', $parameters = []);
});
});

Laravel: always adding user/{id} to routes or different method

I have a Laravel application. In the routes file, I have
Route::group(['prefix' => 'user'], function () {
Route::group(['middleware' => ['auth', 'roles'], 'roles' => ['buyer']], function() {
Route::get('dashboard/buyer', ['as' => 'buyer_dashboard', 'uses' => 'User\Buyer\DashboardController#index']);
});
Route::group(['middleware' => ['auth', 'roles'], 'roles' => ['seller']], function() {
Route::get('dashboard/seller', ['as' => 'seller_dashboard', 'uses' => 'User\Seller\DashboardController#index']);
});
});
I have a middleware that basically checks if the id as supplied in the route, is the same as the current logged in user. If this is not the case, I return an error page. The reason for having this is that I want to prevent that a user can access the dashboard of another user. I also want to prevent that a user can place a bid for someone else (by changing the id in the http request)
The issue is that in the first route, the id is referring to the user. In the second route, the id is referring to the lot id.
Am I obliged to change the second route to:
Route::get('{id}/lot/{lot}/bid/create', ['as' => 'buyer_lot_bid_create', 'uses' => 'User\Buyer\BidsController#create']);
where the first id refers to the user so that I can check the id of the user?
Or is there another way to prevent users from accessing other users pages without explicitly passing the user/{id} in the route?
Doing this in a middleware sounds like a bad idea. You've already come up against one exception to your middleware rule, and you'll certaily come across more.
There's two ways to do this:
Using laravel's built in authorisation tools: https://laravel.com/docs/5.1/authorization
If the check is that the "id as supplied in the route, is the same as the current logged in user" then there's no need to pass the user's id in via the route at all. The user can just visit eg. /dashboard/buyer with no params in the route. Who's dashboard are they visiting? The one of the logged in user, of course. So there's no way for a user to even try to visit another user's dashboard. Likewise with bidding. You can make your bid endpoint so that the bidder's id is not passed in via a route - it's just set to the id of the logged in user in your controller method. So again, there's no way to even try to bid on behalf of another user.
class AuthServiceProvider extends ServiceProvider
{
public function boot(GateContract $gate)
{
$this->registerPolicies($gate);
$gate->define('see-profile', function ($user, $profile) {
return $user->id === $profile->user_id;
});
}
Controller:
public function profile($id)
{
$post = Profile::findOrFail($id);
if (Gate::denies('see-profile', $profile)) {
abort(403);
}
}

Laravel Conditional route filter

Hey guys could you please help me? This one is driving me crazy...
Let's say that I have a method for checking if the user is an admin or not:
public function isAdmin()
{
return Auth::user()->role === 'admin';
}
Then I attach it to a route filter:
Route::filter('admin', function($route, $request)
{
if ( ! Auth::user()->isAdmin()) {
Notification::error('No permission to view this page!');
return Redirect::back();
}
});
Now, I just pass it to the route group
Route::group(array('before' => 'admin'), function()
{
Route::post('users/{id}/update_password', 'UserController#update_password');
Route::post('users/{id}/delete', 'UserController#force_delete');
Route::delete('users/{id}', array('as' => 'users.destroy', 'uses' => 'UserController#destroy'));
Route::post('users/{id}/restore', 'UserController#restore');
Route::get('users/create', array('as' => 'users.create', 'uses' => 'UserController#create'));
Route::post('users', array('as' => 'users.store', 'uses' => 'UserController#store'));
Route::get('users/{id}/edit', array('as' => 'users.edit', 'uses' => 'UserController#edit'));
Route::put('users/{id}', array('as' => 'users.update', 'uses' => 'UserController#update'));
});
The question here is how do I allow a user to bypass this filter if for example he's trying to update it's own profile page an obviously he's not and admin?
I just want to block all access to the users routes for nonadmins but allow the user to edit/update etc on his own profile but allow the admin to do that too.
Could you please point me to the right direction?
You can get the related request segment to check it in your filter:
Route::filter('admin', function($route, $request)
{
if ( ! Auth::user()->isAdmin() && Auth::user()->username !== Request::segment(2)) {
Notification::error('No permission to view this page!');
return Redirect::back();
}
});
There are a few ways to do this, but having a filter that checks the request segments against the currently authenticated user isn't the best way.
Choice Number 1
You simply check that a user is auth'd (use the auth filter), and then in the controller itself you check whether or not the user is an admin, and/or it's their profile.
Choice Number 2
Define a secondary sets of routes specifically for a user modifying their own profile, that doesn't follow the /user/{id}/* pattern.
Route::group(['before' => 'admin'], function() {
// admin routes here
}
Route::group(['prefix' => '/me'], function() {
Route::post('/update_password', 'UserController#update_password');
Route::post('/delete', 'UserController#force_delete');
// etc
}
This would mean that to edit their own profile, they could simply go to /me/edit rather than /user/{id}/edit. To avoid issues like repeating the same code, or errors because an argument is missing, you could do something like this in your controller.
private function getUserOrMe($id)
{
return $id !== false ? User::find($id) : Auth::user();
}
public function edit($id = false)
{
$user = $this->getUserOrMe($id);
}
I recently used this particular method for an API. Sure it requires defining the routes again, but providing that you've set them up with groups that make use of the prefix option, it's a copy and paste job, plus, there are routes an admin would have that a user wouldn't.
Either way, filters weren't intended to do complex logic, but rather, to provide a level of base logic and protection for routes. Logic that identifies whether the current uri is that of the currently logged in user, is something better handled in a controller.

Resources