Proper way to load owned nested-resources in Laravel API - php

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?

Related

Setting up a kind of BelongsToThrough relationship in Laravel

Here is my current situation:
I have a Task model.
Tasks have owners (a belongsTo relationship)
Owners have accounts (yet another belongsTo relationship)
I'd like to set up a "belongsToThrough" relationship from Tasks to Accounts.
My first solution was to define a relationship in the Tasks model, like this:
public function account(): BelongsTo
{
return $this->owner->account();
}
With it I could call $task->account and retrieve a task's account easily. The problem is that this doesn't work with load/with, which in turn causes problems because I can't refresh() a task that has had the account loaded in (because refresh uses load). The error just states Trying to call account() on null which was honestly expected.
My second solution was to change the relationship method to:
public function account(): BelongsTo
{
return $this->owner()->first()->account();
}
With this, I can also simply call $task->account and retrieve the model, and when loading, it doesn't work (returns null), but also doesn't throw any errors. I don't need to load this relationship in, it just happens that sometimes I need to refresh models and having the load method throw an error is not ok.
In summary
What I'm looking for is kind of a BelongsToThrough, as a Task would BelongTo an Account through an Owner (User). Is there a way to do this that works using both $task->account and $task->load('account'). Before you tell me I can load it using owner.account, I know that, but refresh() will do it automatically with load('account') so I need it to work like that, not with the dot notation.
To get it working with load(), you'll need to define an account relationship on the owner model, if you haven't done so already. Like this:
public function account() :BelongsTo
{
return $this->belongsTo(AccountsTable);
}
Then use dot notation when calling load() on your task model like:
$task->load('owner.account');
You can do that using eager loading
public function account()
{
return $this->belongsTo('App\ParentModel', 'foreignkey', 'localkey');
}
After that you can easily fetch relation data with load/with.
Thanks,

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

Laravel - Refactoring User Permission "Gate::Define" Code Into Easier to Read Code

So what i'm basically trying to do is refactor my long bit of code to something more simpler. I found this snippet of code at this website and I don't really understand what's going on inside the code. I don't think that this snippet of code will work considering I am using different policies and methods then what's standard.
Code Snippet From Site:
//PermissionsServiceProvider.php
public function boot()
{
Permission::get()->map(function($permission){
Gate::define($permission->slug, function($user) use ($permission){
return $user->hasPermissionTo($permission);
});
});
}
Can someone please explain what exactly is going on in this bit of code?
My Code:
// Posts Policy
Gate::define('post.view', 'App\Policies\Blog\PostsPolicy#view');
Gate::define('post.create', 'App\Policies\Blog\PostsPolicy#create');
Gate::define('post.update', 'App\Policies\Blog\PostsPolicy#update');
Gate::define('post.delete', 'App\Policies\Blog\PostsPolicy#delete');
Gate::define('post.publish', 'App\Policies\Blog\PostsPolicy#publish');
Gate::define('post.edit', 'App\Policies\Blog\PostsPolicy#edit');
Gate::define('post.global', 'App\Policies\Blog\PostsPolicy#global');
// Categories Policy
Gate::define('category.view', 'App\Policies\Blog\CategoriesPolicy#view');
Gate::define('category.create', 'App\Policies\Blog\CategoriesPolicy#create');
Gate::define('category.update', 'App\Policies\Blog\CategoriesPolicy#update');
Gate::define('category.delete', 'App\Policies\Blog\CategoriesPolicy#delete');
Gate::define('category.edit', 'App\Policies\Blog\CategoriesPolicy#edit');
Gate::define('category.global', 'App\Policies\Blog\CategoriesPolicy#global');
// Tags Policy
Gate::define('tag.view', 'App\Policies\Blog\TagsPolicy#view');
Gate::define('tag.create', 'App\Policies\Blog\TagsPolicy#create');
Gate::define('tag.update', 'App\Policies\Blog\TagsPolicy#update');
Gate::define('tag.delete', 'App\Policies\Blog\TagsPolicy#delete');
Gate::define('tag.edit', 'App\Policies\Blog\TagsPolicy#edit');
Gate::define('tag.global', 'App\Policies\Blog\TagsPolicy#global');
// Parts Section Policy
Gate::define('part.section.view', 'App\Policies\Parts\PartSectionsPolicy#view');
Gate::define('part.section.create', 'App\Policies\Parts\PartSectionsPolicy#create');
Gate::define('part.section.update', 'App\Policies\Parts\PartSectionsPolicy#update');
Gate::define('part.section.delete', 'App\Policies\Parts\PartSectionsPolicy#delete');
Gate::define('part.section.edit', 'App\Policies\Parts\PartSectionsPolicy#edit');
Gate::define('part.section.global', 'App\Policies\Parts\PartSectionsPolicy#global');
// Parts Policy
Gate::define('part.view', 'App\Policies\Parts\PartsPolicy#view');
Gate::define('part.create', 'App\Policies\Parts\PartsPolicy#create');
Gate::define('part.update', 'App\Policies\Parts\PartsPolicy#update');
Gate::define('part.delete', 'App\Policies\Parts\PartsPolicy#delete');
Gate::define('part.edit', 'App\Policies\Parts\PartsPolicy#edit');
Gate::define('part.global', 'App\Policies\Parts\PartsPolicy#global');
// Admin Management Policy
Gate::define('admin.global', 'App\Policies\AdminManagementPolicy#global');
// User Management Policy
Gate::define('user.global', 'App\Policies\UserManagementPolicy#global');
Is there a way to do this as a foreach loop from my permissions table? Here's some Pseudo code:
foreach($permissions as $permission) {
Gate::define($permission->slug, 'App\Policies\' . $permission->category . 'Policy#' . $permission->name);
}
Question: Any way to make my code more compact and easier to read like the code snippet from the website?
First of all, the author of that article did not use policies at all, he created a permissions table and then bound the permissions he created to laravel gates by the code snippet
Permission::get()->map(function($permission){
Gate::define($permission->slug, function($user) use ($permission){
return $user->hasPermissionTo($permission);
});
});
Let's break it line by line
Permission::get() // Query all permissions defined in permissions database table
->map(function($permission){ // Foreach permission do the following
Gate::define($permission->slug, // Create new gate with the permission slug
function($user) use ($permission){
return $user->hasPermissionTo($permission); // the user table has many to many relation with permissions table, here we only check if $user is associated with $permission
});
});
To make your code more dynamic, I suggest you to do the following:
Database structure
Create permission database table
Create roles database table
Create permission_role pivot database table
Create role_user pivot database table
Define Relationships
Role has many permissions ( many to many relationship, define it with belongsToMany )
Permission belongs to many roles ( many to many relationship, define it with belongsToMany )
User has many roles ( many to many relationship, define it with belongsToMany )
Reduce the number of global permissions
By utilising Gate::before you can allow specific user who has global or root permission to authorise all defined abilities:
Gate::before(function ($user, $ability) {
if ($user->hasPermission('root-access')) {
return true;
}
});
If you implement the database permissions you no longer need to create policies for every model, and the gates will be defined using the above code dynamically.
Personally, your existing code is fine. It works. It is readable. While it might become more verbose as your app grows, it also might not. So why improve it?
That said, here are some ideas. Most of your code is a mapping between permission and policy implementation. For example 'part.view' maps to 'App\Policies\Parts\PartsPolicy#view. The "weight" of this mapping can't be removed: it can only be moved.
You might consider moving it to a simpler configuration file, something that looks like this:
// config/permission-map.php
<?php return [
'post.view' => 'App\Policies\Blog\PostsPolicy#view',
'post.create' => 'App\Policies\Blog\PostsPolicy#create',
'post.update' => 'App\Policies\Blog\PostsPolicy#update',
'post.delete' => 'App\Policies\Blog\PostsPolicy#delete',
// etc...
];
Then in your boot you read that config and iterate:
// boot permissions
$permission_map = require_once('config/permission_map.php');
foreach ($permission_map as $permission => $policy_implementation) {
Gate::define($permission, $policy_implementation);
}
Advantage: adding a new policy mapping only changes the most salient information, and you don't have to think about how to make that mapping happen - today that is Gate::define but tomorrow maybe it's Sentry::policy. Additionally, by separating data from the code, you can test the code more freely.
Another approach could be annotations: in the DocBlock of your policy implementation, you write your own annotation syntax, which you then parse and compile into the configuration file. Along the lines of
namespace App\Policies\Blog;
class PostsPolicy {
/**
* #permission post.view
*/
public function view() { /* ... */ }
}
I, personally, am not a fan of this: it adds a layer of inner framework whose value I find it hard to measure.

Laravel - How to CRUD records with ownership?

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.

Best way to filter access to controller actions according to a specific client id

Using CakePHP 2.2, I am building an application in which each client has it's own "realm" of data and none of the other data is visible to them. For example, a client has his set of users, courses, contractors and jobs. Groups are shared among clients, but they cannot perform actions on groups. All clients can do with groups is assign them to users. So, an administrator (using ACL) can only manage data from the same client id.
All my objects (except groups, of course) have the client_id key.
Now, I know one way to get this done and actually having it working well, but it seems a bit dirty and I'm wondering if there is a better way. Being early in the project and new to CakePHP, I'm eager to get it right.
This is how I'm doing it now :
1- A user logs in. His client_id is written to session according to the data from the user's table.
$user = $this->User->read(null, $this->Auth->user('id'));
$this->Session->write('User.client_id', $user['User']['client_id']);
2- In AppController, I have a protected function that compares that session id to a given parameter.
protected function clientCheck($client_id) {
if ($this->Session->read('User.client_id') == $client_id) {
return true;
} else {
$this->Session->setFlash(__('Invalid object or view.'));
$this->redirect(array('controller' => 'user', 'action' => 'home'));
}
}
3- Im my different index actions (each index, each relevant controller), I check the client_id using a paginate condition.
public function index() {
$this->User->recursive = 0;
$this->paginate = array(
'conditions' => array('User.client_id' => $this->Session->read('User.client_id'))
);
$this->set('users', $this->paginate());
}
4- In other actions, I check the client_id before checking the HTTP request type this way.
$user = $this->User->read(null, $id);
$this->clientCheck($user['User']['client_id']);
$this->set('user', $user);
The concept is good - it's not 'dirty', and it's pretty much exactly the same as how I've handled situations like that.
You've just got a couple of lines of redundant code. First:
$this->Auth->user('id')
That method can actually get any field for the logged in user, so you can do:
$this->Auth->user('client_id')
So your two lines:
$user = $this->User->read(null, $this->Auth->user('id'));
$this->Session->write('User.client_id', $user['User']['client_id']);
Aren't needed. You don't need to re-read the User, or write anything to the session - just grab the client_id directly from Auth any time you need it.
In fact, if you read http://book.cakephp.org/2.0/en/core-libraries/components/authentication.html#accessing-the-logged-in-user it even says you can get it from outside the context of a controller, using the static method like:
AuthComponent::user('client_id')
Though it doesn't seem you'll be needing that.
You could also apply the client_id condition to all finds for a Model by placing something in the beforeFind function in the Model.
For example, in your User model, you could do something like this:
function beforeFind( $queryData ) {
// Automatically filter all finds by client_id of logged in user
$queryData['conditions'][$this->alias . '.client_id'] = AuthComponent::user('client_id');
return $queryData;
}
Not sure if AuthComponent::user('client_id') works in the Model, but you get the idea. This will automatically apply this condition to every find in the model.
You could also use the beforeSave in the model to automatically set that client_id for you in new records.
My answer may be database engine specific as I use PostgreSQL. In my project I used different schema for every client in mysql terms that would be separate database for every client.
In public schema (common database) I store all data that needs to be shared between all clients (objects that do not have client_id in your case), for example, variable constants, profile settings and so on.
In company specific models I define
public $useDbConfig = 'company_data';
In Controller/AppController.php beforeFilter() method I have this code to set schema according to the logged in user.
if ($this->Session->check('User.Company.id')) {
App::uses('ConnectionManager', 'Model');
$dataSource = ConnectionManager::getDataSource('company_data');
$dataSource->config['schema'] =
'company_'.$this->Session->read('User.Company.id');
}
As you see I update dataSource on the fly according to used company. This does exclude any involvement of company_id in any query as only company relevant data is stored in that schema (database). Also this adds ability to scale the project.
Downside of this approach is that it creates pain in the ass to synchronize all database structures on structure change, but it can be done using exporting data, dropping all databases, recreating them with new layout and importing data back again. Just need to be sure to export data with full inserts including column names.

Categories