In my laravel app I have multiple user accounts who have resources that are assigned to them. Say, for example, a "payment". To edit or view a payment a user would visit the /payment/edit/{payment} route (where payment is the payment ID).
Although I have an auth filter to stop un-logged in users from accessing this page there is nothing to stop, for example, user 1 from editing user 2's payment.
Is there a filter I can user that checks which user a payment (or any other resource) belongs to prevent this kind of issue?
[I am using Laravel's model bindings which automatically fetches the model specified by the route rather than me get it in the controller using eloquent.]
No such filter exists by default, however you can easily create one (depending on how your database is set up). Within app/filters.php, you may do something like this:
Route::filter('restrictPermission', function($route)
{
$payment_id = $route->parameter('payment');
if (!Auth::user()->payments()->find($payment_id)) return Redirect::to('/');
});
This compares the currently logged in user's payment_id (in your database) to the {payment} argument passed into the route. Obviously, depending on how your database is set up (for instance if the payment_id is in a separate table) you need to change the conditional.
Then, apply the filter to your route:
Route::get('/payment/edit/{payment}', array('before' => 'restrictPermission'));
One way is to place a where statement in every relevant query. Although not very pretty, it works.
$payment = Payment::where('user_id', '=', Auth::user()->id)->find($id);
It's also possible to use url filters like seeARMS is suggesting, however I think it's not very elegant. The most logical place to nest such logic is in the model itself. One possibility is to use model events, but this gives you only the option to intercept update, insert or delete statements, not selects. This might change in the future. Maybe you could use boot() event, but I'm not sure if this is gonna work.
Last but not least you could use query scopes.
class Payment extends Eloquent {
public function scopeAuthuser($query)
{
return $query->where('user_id', '=', Auth::user()->id);
}
}
and in the queries you attach the scope
Payment::authuser()->find($id);
You could do this on a base Model and extend from it, so you have that method in all your relevant models.
Consider using Laravel Policies:
https://laravel.com/docs/6.x/authorization#policy-methods
<?php
namespace App\Policies;
use App\Post;
use App\User;
class PostPolicy
{
/**
* Determine if the given post can be updated by the user.
*
* #param \App\User $user
* #param \App\Post $post
* #return bool
*/
public function update(User $user, Post $post)
{
return $user->id === $post->user_id;
}
}
By policies you can control if given record could be edited by logged user or not.
Cheers!
Related
I have a Location Model, which contains two properties: ID and Name.
To edit this Model, I have set up this route:
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit');
I set up very simple permissions: In the AuthServiceProvider I am checking in the boot method the following
Gate::before(function ($user, $permission) {
if ($user->permissions->pluck('name')->contains($permission)) {
return true;
}
});
Where permission is a Model that contains an ID and a name, mapped via a permission_user table.
I have these permissions set up:
edit_los_angeles
edit_new_york
edit_boston
plenty_of_other_permissions_not_related_to_location
After all this rambling, my actual question:
How can I tie these permissions to the edit the location?
The problem that I am facing is, that a given user is not allowed to edit all locations but may only be allowed to edit one location. Only the user with permission edit_los_angeles would be allowed to edit the Location with the name Los Angeles.
So I cannot group this into one permission like edit_location and add this to my route ->middleware('can:edit_location').
Instead, I would need something like this, I guess:
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit')->middleware('can:edit_los_angeles');
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit')->middleware('can:edit_new_york');
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit')->middleware('can:edit_boston');
...obviously this would not work.
What would be your approach to tackle this dilemma? :-)
Maybe I am doing something completely wrong and there is a better Laravel-Way of doing this?
Thank you very much for your help in advance!
I am using Laravel 6.0 :-)
Two assumption for my approach to work, use model binding in the controller (you should do that no matter what). Secondly there needs to be a relation between location and the permission it needs, something similar to the slug you suggested.
Your controller function would look something like this. Adding a FormRequest is a good approach for doing this logic.
class LocationController {
public function edit(EditLocationRequest $request, Location $location) { // implicit model binding
...
}
}
For ease of use, i would also make a policy.
class LocationPolicy
{
public function edit(User $user, Location $location) {
return $user->permissions->pluck('name')
->contains($location->permission_slug); // assuming we have a binding
}
}
Remember to register policy in the AuthServiceProvider.php.
protected $policies = [
Location::class => LocationPolicy::class,
];
Now in your form request consume the policy in the authorize method. From here you are in a request context, you can access user on $this->user() and you can access all models that are model binding on their name for example $this->location.
class EditLocationRequest
{
public function authorize(): bool
{
return $this->user()->can('edit', $this->location);
}
}
Now you should be able to only have a single route definition.
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit');
EDIT
Withouth the form request if you use the trait AuthorizesRequests you can do the following. This will throw an AuthorizationException of it fails.
use AuthorizesRequests;
public function edit() {
$this->authorize('edit', $location);
}
If you have a requirement based upon the location relationship, then you will need to capture that relationship in the data. A good starting point to this would be to add a pivot table specific for these editing permissions. Consider a table, location_permissions, with a user_id and a location_id. You could then modify or add permission middleware to do a check for a record in this table once you have a specific user and location.
Edit: to answer the question about implementation of middleware,
The crux of the implementation would likely be solved by defining a relationship on the user model to location via this new pivot table.
I would recommend then adding an additional method which consumes the new locations relationship to the model along the lines of
public function canEditLocation(Location $location): bool {
return $this->locations
->where('location_id', '=', $location->id)
->count() > 0;
}
And the actual middleware something along these lines:
public function handle($request, Closure $next, $location)
{
if (! $request->user()->canEditLocation($location)) {
\\handle failed permission as appropriate here.
}
return $next($request);
}
My middleware parameters knowledge is rusty, but I believe that is correct as defined at https://laravel.com/docs/master/middleware#middleware-parameters
i'm using Laravel as my PHP framework. its a convention to put index show store ... functions in controllers.
i have 2 types of users(Admin & normal user). lets assume there is an Order(in restaurant) model and i want to implement index function for its controller.
a user can have more than one Order.
what i need is that this function:
if admin is calling this API: returns all Orders
if normal user is calling this API: returns just Orders owned by this user
i searched but i couldn't find anything(tbh i didn't know what to search).
once i did this as below which i didn't like because it looks two different functions gathered in one:
if ($user->role == admin) {
// fetch all orders
} else if ($user->role == normal_user) {
// just find user orders
}
so my question is what's best approach to achieve what i want?
Such a REST API endpoint is typically a search allowing multiple filters, sorting and pagination. If so it is completly fine to apply different defaults for filters and also restrict filters to roles.
I would auto apply a filter user=currentUser for missing admin role and return a forbidden if a user without the admin role tries to apply a user filter for a different user.
With this approach you give admins also the functionality to search for offers of a specific user and you only need one search api to be used by the controller.
Why don't use an if statement?
You could make a scope on the model but then you'll still have an if.
What about this?
if ($user->role == admin) {
Order::all();
} else if ($user->role == normal_user) {
$user->orders()->get();
}
Or make it an inline if
$user->role == admin ? Order::all() : $user->orders()->get();
IMO the best practice here is to make a different Admin/OrderController.php
Then with middleware check wat, the role of the user is, and then redirect them to the admin controllers.
Since you'll probably also want an update and delete, or other functions only accesible by an Admin
I had a similar question myself a while ago and ended up with this strange solution to avoid that if/else block.
Assumptions
I assumed the existence of an helper method in the User model called isNot($role) to verify the if the user's role matches or not the given one.
This is just an example to give the idea of the check, but you should implement the condition as you like.
Second assumption I made is that each order has a user_id field which will reference the owner of that order though his id (FK of 1:N among user and order).
Implementation
public function index(Request $request)
{
$orders = Order::query()
->when($request->user()->isNot('admin'), function ($query) use ($request) {
return $request->user()->orders();
// Or return $query->where('user_id', $request->user()->id);
})
->paginate();
return OrderResource::collection($orders);
}
The when method is the key here. Basically you call it like: when($value, $callback) and if $value is false the callback won't be executed, otherwise it will.
So for example, if the user is not an admin, you will end up executing this query:
Order::paginate();
that would fetch all the order with pagination (note that you could swap paginate with get.
Otherwise, the callback is gonna be executed and you will execute the paginate method on $request->user()->orders(); (orders called like a method is still a query builder object, so you can call paginate on it).
The query would be:
$request->user()->orders()->paginate();
If you instead opted for the second solution in the callback you would basically add a where condition (filtering on the user_id of the orders) to the main scope to get only the user's orders.
The query would be:
Order::query()->where('user_id', $request->user()->id)->paginate();
Finally, to better control what's sent back as response I use Laravel's API Resource (and I really suggest you to do so as well if you need to customize the responses).
NOTE: The code might have syntax and/or logical errors as it was just an on the fly edit from production code, and it hasn't been tested, but it should give an overall idea for a correct implementation.
it would be better to include the if/else in your order modal like this:
class Order extends Model {
....
static function fetchFor (User $user) : Collection
{
return $user->isAdmin() ? self::all() : self::where("user_id",$user->id);
}
}
then you can call this method on your controller
public function index()
{
return view('your-view')->with('orders',Order::fetchFor(Auth::user())->get())
}
You can create scope in Order class...
For example you have field user_id in Order, for detect user
class Order
{
...
public function scopeByRole($query)
{
if (!Auth::user()->isAdmin())
$query = $query->where('user_id', Auth::user()->id);
return $query;
}
}
in you controller just get all Orders with scope:
$orders = Order::byRole()->get();
it return you orders by you role
Also you need have in class User function for detect role, example
class User
{
public function isAdmin()
{
// you logic which return true or false
}
}
I am using backpack to created admin panel in my project. I have two types of user one is Superadmin and second is admin. I just wanted to give the permissions to superadmin as he can list,add,edit all the rows from database..
but the admin can only edit,delete and list those rows created by himself..
So please help me, I am new in laravel backpack??
Filter the results you show in the setup() method of your entity's CrudController, then disallow access to update/destroy methods.
You result could look something like this:
public function setup()
{
// take care of LIST operations
if (\Auth::user()->hasRole('admin')) {
$this->crud->addClause('where', 'author_id', '=', \Auth::user()->id);
}
}
Additionally, you need to place checks inside your update() and destroy() methods, to not allow an admin to delete someone else's entry.
// place this both inside your update() and inside your destroy() method
if (\Auth::user()->hasRole('admin') && $this->crud->entry->author_id!=\Auth::user()->id) {
abort(405);
}
Hope it helps.
I´m building my API (makes and models) and I want to have nested resources (not sure if this is correct Restfully speaking)
/makes/ferrari/models
/makes/ferrari/models/f40
I defined the following route
Route::resource('makes.models', 'ModelsController');
and the ModelsController.php
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index()
{
$data = Models::all();
return response()->json($data);
}
/**
* Display the specified resource.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function show($id)
{
$data = Models::find($id);
return response()->json($data);
}
and the models model (yeah I need to change the name)
class Models extends Model
{
public function make()
{
return $this->belongsTo('App\Make');
}
}
my problem is that even if the route works it returns all models in the db (not only ferraris) where should I define that relationship? is not automatic?
I have 2 tables makes (id, name), models (id, name, make_id)
thank you!
The resource route will define the following routes:
Method Path Action
GET /makes/{make}/models index
GET /makes/{make}/models/create create
POST /makes/{make}/models store
GET /makes/{make}/models/{id} show
GET /makes/{make}/models/{id}/edit edit
PUT /makes/{make}/models/{id} update
DELETE /makes/{make}/models/{id} destroy
Your request /makes/ferrari/models will not match any of those routes (as your show parameter only takes one parameter). You may request for /makes/models/1 to call show, but you are practically missing the route for this, as the nested route does not provide it.
If you say that you always get all items, you are very likely hitting the index action instead of show.
If you want to query your models with /makes/ferrari/models/f40, you would need a route like this:
Route::get('/makes/{make}/model/{model}', 'ModelsController#show');
Which is already part of the resource route created for you.
Now, in your show controller, use the make and model parameters to find the correct dataset:
public function show($make, $model)
{
$data = Model::with('makes')
->whereName($model)
->whereHas('makes', function ($query) use ($make) {
$query->where('name', '=', $make);
})->get();
return response()->json($data);
}
Laravel doesn't automatically do that for you.
Update: Route model binding
You might want to check out https://laravel.com/docs/5.3/routing#route-model-binding for a more sophisticated way of doing this. You can set your route key name in both of your models overwriting the getRouteKeyName() method and returning 'name' in this case, telling Laravel to use the name column instead of the id.
You can also bind parameters in your routes specifically to a custom resolution logic by doing something like
$router->bind('model', function ($value) {
return Model::where('name', $value)->first();
});
and then every time you use {model} in your routes, it will use the name instead of the id.
Use slugs
However, be advised that you have to make absolutely sure that the names stored in the database for model and make are sluggified so that they are suited for use in URLs. If necessary, you may possibly do that in your bind as shown above, returning
return str_slug(Model::where('name', $value)->first());
This is untested, however, so it might or might not work.
Hope that helps :-)
When using nested resources, all your controller actions will receive an additional first parameter (the parent resource identifier). So you need to update your controller actions accordingly:
public index($make) {
$make = Make::with('models')->where('name', $make)->firstOrFail();
return view('models.index', compact('make'));
}
public show($make, $model) {
$make = Make::with('models')
->where('name', $make)
->firstOrFail();
$model = $make->models()
->where('name', $make)
->firstOrFail();
return view('models.show', compact('make', 'model'));
}
It should be the same with your other controller actions.
Note that I made assumptions regarding the structure of your database.
What would be the best way to create a relationship if it doesn’t exist already, within Eloquent, or at least a central location.
This is my dilemma. A User must have a Customer model relationship. If for whatever reason that customer record doesn’t exist (some bug that stopped it from being created) - I don’t want it to throw errors when I try to retrieve it, but I also request the customer object in multiple locations so I don’t want to test for existence in all those places.
I thought of trying the following in the User model:
public function getCustomerAttribute($value) {
// check $value and create if null
}
But that doesn’t work on relationships, $value is null.
EDIT
I already create a customer upon user creation, but I have come across a situation where it wasn't created and caused exceptions in many places, so I want to fallback.
User::created(function($user) {
$customer = Customer::create([
'user_id' => $user->id
]);
});
Is it possible for you to assume when a user is created that a customer needs to be created as well? If the rest of your system depends on this assumption I would make a model event.
use App\{User, Customer}; // assuming php7.0
UserServiceProvider extends ServiceProvider
{
/**
* Boot
*/
public function boot()
{
// on a side note, we're using "created" not "creating" because the $user->id needs to exist in order to save the relationship.
User::created(function($user) {
$customer = Customer::create([
'user_id' => $user->id
]);
});
}
}