Laravel - How to CRUD records with ownership? - php

Let's say I've multiple users - Admin, Manager, User. User can CRUD records owned by him. Manager can CRUD records owned by him and his Users. Admin can CRUD records of all. How to achieve this in Laravel?

If you have only one role use middleware.
If you have multiple roles just like you are saying you have use gates/policies provided by Laravel out of the box.
To handle admin you will use before filter
public function before($user, $ability)
{
return ($user->is_admin) ? true : null; //return null so we continue authorising further
}
To handle manager || user you will use something along these lines (most tricky one):
public someCRUDaction(User $current, Item $item) {
return $item->created_by == $current->id || $current->users->contains('id', $item->created_by); // you need to handle logic if item belongs to manager
}
Note: $item->created_by == $current->id is self explanatory and handles if current user is owner of item
How to use policies (I would go with policies) and gates:
https://laravel.com/docs/5.4/authorization#writing-policies
Remember that you don't want to validate anything just simply can $current user do action X?
Using policies you will need to check if user can do action X at least two times (in views and in controllers/routes/customized form requests - where you handle validation).

You can create a middleware which verify the role of the current user and the role of the selected user for the action.

Related

How to pass multiple user ids to the controller in a Laravel policy file

In my Product Policy file I have the following:
public function change_customer_pricing(User $user) {
return $user->id == 2;
}
How do I return more than one user ID to the Controller?
My controller:
public function change_customer_pricing($ProdID) {
$user = Auth::user();
//dd($user->can('change_customer_pricing', Product::class));
if (!$user->can('change_customer_pricing', Product::class))
return redirect()->route('home')->with('status', 'You are not Authorized to access')->with('code', 'red');
...
public function change_customer_pricing(User $user){
return in_array($user->UserID,[2,12,14]);
}
If you're looking to determine if a User has authorisation to perform an action on a Product, you need a relationship between the User and Product and then check the relationship.
Say you have a user_id field on your Product which holds the id of the User that created that Product for example and only that User is allowed to update or delete the Product. You would want to check the id of the authenticated User matches the user_id of the Product.
public function change_customer_pricin(User $user, Product $product)
{
return $user->id === $product->user_id;
}
Then you can use any of the baked in authorisation functions with Laravel. As you're wanthing to check if a User is authorised from a controller, you can do:
public function change_customer_pricing($ProdID)
{
$product = Product::find($ProdId);
$this->authorize('change_product_pricing', $product);
}
As a side note, you might want to consider using route model binding in your controller.
Update
As your Product doesn't have a relationship to a User and you're looking to authorise multiple Users, you could check id of the currently authenticated User against an array:
public function change_customer_pricin(User $user)
{
return in_array($user->id, [1, 2, 3, 4];
}
This is the quickest and simplest solution. However, it is not very maintainable as you'd have to add/remove as requirements changed.
A better solution would be to use roles and permissions to determine authorisation. You define permissions which are used scope access then you can associate (assign) permissions directly to a User or to a Role (and the a Role to a User). When an action is performed, you check if a User has the required Role or Permission.
There are a few packages available which provide such functionalty, a very popular option is Spatie Permission and another popular choice is Laratrust.

Laravel eloquent model relationship utility method fails first time

In a fresh installation of Laravel 8.20.1, I have created two models Company and User with a pivot table between to facilitate a many-to-many relationship. The pivot has a role attribute.
I can add a user to a company using:
$company->users()->attach($user);
I've added a utility method addUser so that I can first check existence of the relationship to avoid duplication:
public function addUser(User $user) {
if($this->users->contains($user)) {
// if the user already has a role, update it
Log::info("User #{$user->id} present - updating");
$this->users()->updateExistingPivot($user, ['role' => 'user'], true);
} else {
// if the user doesn't have a role, add it
Log::info("User #{$user->id} not present - adding");
$this->users()->attach($user, ['role' => 'user'], true);
}
}
The first time I run this using a refreshed database, it should see that the user is not yet related to the company and run the else part of the switch to add a new user. Running this in Tinker, it appears to do this - and the logs show 'User #1 not present - adding' - but when I check for presence using contains, it returns false:
$user = User::factory()->create();
$company = Company::factory()->create();
$company->addUser($user);
print_r($company->users->contains($user)); //false
I've tried logging the queries for this function, and I they look fine - one for checking existence of the user, a second for inserting the pivot.
Also, I can see a pivot record for user #1 and company #1, and if I then test this in Tinker, I get true:
print_r(Company::find(1)->users->contains(User::find(1))); // true
It's almost as if the database is running async, which I know isn't the case in PHP. I'm using Sqlite v3.31.0.
The issue is definitely in my addUser method, as if I replace this call in Tinker with calling attach directly, it works.
I'm really keen to use this utility method (and others), because:
I want to avoid multiple pivot records for the same company/user (without using compound indexes)
I need several more utility methods such as addAdmin, addOwner, etc

RESTful API different responses based on user roles

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
}
}

Get data based on user login in laravel backpack

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.

Proper way to load owned nested-resources in Laravel API

Using Laravel models, I have built the following structure
user-1
company-1
store-1
store-2
company-...
store-1
company-N
store-1
store-2
store-n
user-2
company-5
store-12
user-3
company-8
store-15
company-9
store-21
That reads: an user have N companies and each company have N stores.
I have the following routes for that
$api->resource('companies', 'App\Http\Controllers\v1\CompaniesController');
$api->resource('companies.stores', 'App\Http\Controllers\v1\StoresController');
Right now, my CompaniesController is listing the companies as follows:
public function index() {
return $this->response->collection(
JWTAuth::parseToken()->authenticate()->companies, new CompanyTransformer
);
}
Which I don't think it's appropriate, but it's a working code (for that I have posted a Code Review).
Now, going down the rabbit hole, we have the next controller: StoresController
public function index($company) {
$company = JWTAuth::parseToken()->authenticate()->companies->find($company);
if(empty($company))
throw new NotFoundHttpException();
return $this->response->collection(
JWTAuth::parseToken()->authenticate()->companies->find($company)->stores, new StoresTransformer
);
}
Here is where I'd say it's no longer an acceptable Working Code. In order to find all Stores from a given company, I have to find() between the user companies a specific company and check if it's not null (it exists) so I can return the proper list of stores. Imagine when I have to list a child of Stores? And if I have child resource of that child? The more I go down, the more ifs I'll have to perform to make sure the user owns that resource.
Am I missing something here? How do people give a list of owned resource given an Authenticated user?

Categories