Get request for the policies in controller constructor - php

I'm setting up policies for my laravel application, and I'm stuck with a problem. I have to put the policy in the constructor of my controller this way:
public function __construct()
{
$this->middleware(['can:viewAny,App\Models\Photo'], ['only' => ['index']]);
$this->middleware(['can:view,photo'], ['only' => ['show']]);
}
Problem is, for the store action, I have to check one of the params sent in the request to check if the user is allowed to post on the related parent. According to the documentation, I could make my Policy this way:
public function store(User $user, int $parentId)
{
$parent = Parent::find($parentId);
return $user->id === $parent->user_id
}
And in the controller:
public function store(Request $request)
{
$this->authorize('store', [$request->parent]);
// The current user can store the photo...
}
But in the example, the authorization is put in the function, and there are no example with the usage of the request when treating the policy as a middleware. Is it even possible? I would have crafted something like:
$this->middleware(['can:store,App\Models\Photo,request->parent'], ['only' => ['store']]);
But that won't work. Thanks a lot if you can help me on this one!

I found how to do it, I forgot about the request() helper. Thus, I can access everything put in the request, and I can call the helper directly inside the policy.
So I can do this in the contructor:
$this->middleware(['can:store,App\Models\Photo'], ['only' => ['store']]);
And in the PhotoPolicy:
public function store(User $user)
{
$input = request()->input();
$parent = Parent::find($input['parent_id']);
return $user->id === $parent->user_id
}

Related

Pass in an id for Authorization Laravel

In my user controller I have an posts function, which gives access to a sub-resource of users. This is accessed through the /users/{id}/posts endpoint.
I want the pass the $id from the request URL into a UserPolicy method:
public function resource($user, $id)
{
return $user->id === $id;
}
My UserController method:
public function posts(Request $request, $id)
{
$this->authorize('resource', $id);
return response()->json(['events' => []], 200);
}
Is there anyway to do this? I notice that Policy methods seem to ignore anything that isn't an object.
Edit:
I am currently using a helper method for this authorization but would like to move it to my Policy to keep all rules together:
public function authorizeResource($id)
{
if ((int)$id !== (int)$this->auth->user()->id) {
throw new \Exception;
}
}
Laravel needs to know which policy class to use. For that you need to specify the model, in this case passing an array with an instance of user first and then the $id. Laravel uses the spread operator and will inject the $id as a parameter on your callback function.
//UserController.php
public function posts(Request $request, $id)
{
$this->authorize('resource', [User::class, $id]);
return response()->json(['events' => []], 200);
}

Passing variable from middleware to controllers __construct to prevent repeating myself

I'm doing an existence check within a middleware, by checking a route-parameter.
If the check succeeds, I'm attaching it's model to the request to make it available throughout the rest of the request-cycle, application.
// App\Http\Middleware\CheckForExistence.php:
...
public function handle($request, Closure $next)
{
// some checks...
// success
$request->attributes->add([
'company' => $someModel
]);
}
I now have a controller which 'needs' this information in a couple of methods. So my thought was to add it to the construct of the controller and add it as a protected var in the whole controller:
// App\Http\Controllers\MyController.php
<?php
use Illuminate\Http\Request;
class MyController extends Controller
{
protected $company;
public function __construct(Request $request)
{
$this->company = $request->attributes->get('company');
}
public function index()
{
dd($this->company); // returns null
}
}
This controllers index() returns null instead of the give model.
If I change the index() method to:
public function index(Request $request)
{
return $request->attributes->get('company');
}
This returns the model; as expected.
Why is this happening? It looks like the middleware is not run when the controller is constructed.... Is there a way to circumvent it?
Or am I missing the obvious here.....
I could off course repeat myself in each method; but that is not very DRY ;)
You can't access the session or authenticated user in your controller's constructor because the middleware has not run yet, So you can do it like this :
public function __construct()
{
$this->middleware(function ($request, $next) {
$this->company = $request->attributes->get('company');
return $next($request);
});
}
For reasons currently unclear to me, the controller object is constructed before the request changes are reflected in the request object. In short the request is not considered properly constructed when a controller is constructed. This post seems to imply that.
There's two ways to work around this (if for a second we ignore what you're trying to do).
Use request dependency injection
public function index(Request $request)
{
$compary = $request->attributes->get('company');
}
This is not really WET because you're just swapping $this->company with $request->attributes->get('company') it's just a refactor. You should be injecting the request in the controller action anyway and if you don't want to do that you can use the request() helper.
Use a callback middleware in the constructor (Maraboc's answer explains how)
Now if you want a more case specific solution though you can use case specific dependency injection:
If you need to bind a model to a specific route parameter you can use route model binding and add the following in your RouteServiceProvider (or any provider).
Route::bind("companyAsARouteVarName", function () {
// this is why more details in the question are invaluable. I don't know if this is the right way for you.
//checks
// success
return $someModel;
});
Then you will register your route as:
Route::get("/something/{companyAsARouteVarName}", "SomeController#index");
and your controller will be:
public function index(Company $companyAsARouteVarName) {
//Magic
}
Controller constructor will be initialized before middleware execution.
You can get data from Injected $request object in controller functions.

Not working laravel Policy for my UsersUpdate

I have
(1/1) HttpException
This action is unauthorized.
I think all should work fine and I have done it right but maybe not.
My controller method:
public function update(Request $request, Users $uzytkownik)
{
$this->authorize('update', $uzytkownik);
return 1;
}
UsersPolicy that is in App\Policies\:
<?php
namespace App\Policies;
use App\Models\Users;
use Illuminate\Auth\Access\HandlesAuthorization;
class UsersPolicy
{
use HandlesAuthorization;
public function update(Users $user)
{
return true;
// return $user->login === auth()->login;
}
}
And in AuthServiceProvider:
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
'App\Models\Users' => 'App\Policies\UsersPolicy',
];
My Users model lays in App\Models\
When I cut $this->authorize('update', $uzytkownik); this line from controller everything works fine and I see '1', when I add it again HttpException.
What do I have wrong here? Thinking and Thinking, looking, I don't see anything bad here.
please make sure that your route is under auth middlware like this :
Route::group(['middleware' => 'auth'], function () {
// ur update route here
});
or in ur controller constructor like this :
public function __construct()
{
$this->middleware('auth');
}
and also like #Laerte said your update policy method should have another parameter of type user which is the user you want to edit, like this :
public function update(Users $userLoggedIn, Users $uzytkownik)
{
return true;
}
In your Policy, you have to add two parameters: The first one is the user logged in, and the second is the actual parameter. Try this in the Policy:
public function update(Users $userLoggedIn, $user)
{
return true;
}

Laravel resource policy always false

I'm trying to allow user to view their own profile in Laravel 5.4.
UserPolicy.php
public function view(User $authUser, $user)
{
return true;
}
registered policy in AuthServiceProvider.php
protected $policies = [
App\Task::class => App\Policies\TaskPolicy::class,
App\User::class => App\Policies\UserPolicy::class
];
Routes
Route::group(['middleware' => 'auth'], function() {
Route::resource('user', 'UserController');
} );
Blade template
#can ( 'view', $user )
// yes
#else
// no
#endcan
UserController.php
public function profile()
{
return $this->show(Auth::user()->id);
}
public function show($id)
{
$user = User::find($id);
return view('user.show', array( 'user'=>$user,'data'=>$this->data ) );
}
The return is always 'false'. Same for calling policy form the controller. Where do I go wrong?
Answering my own question feels weird, but I hate it when I come across questions without followups.
So after double checking It turned out that if I remove authorizeResource from the constructor:
public function __construct()
{
$this->authorizeResource(User::class);
}
and check for authorization in the controller function:
$this->authorize('view',$user);
everything works.
I must've missed this part when I added $user as a parameter in the policy function. So the user to be viewed is never passed in the authorizeResource method.
Thanks everyone for taking your time to help me.
When you add
public function __construct()
{
$this->authorizeResource(User::class);
}
to your Controller, you have to edit all your function signatures to match it to the class e.g. your show signature has to change from public function show($id)
to public function show(User $user)
After that it should work
Just a different approach here to users viewing their own profile.
First, I will create a route for that
Route::group(['middleware' => 'auth'], function() {
Route::get('profile', 'UserController#profile');
});
Then in the profile function I do
public function profile()
{
$user = Auth::user();
return view('profile', compact('user'));
}
This way, user automatically only views their own profile.
Now, if you want to allow some users to view others' profiles, then you can use Policy. Why? Because I think user should ALWAYS be able to view their own profile. But not all users should view other users profiles.
Solution:
Change the second parameter from #can( 'view', $user ) to #can( 'view', $subject ) and it will work find.
Why:
Because you're doing it the wrong way.
public function view(User $user, $subject){
return true;
}
Just look carefully the policy view method, first parameter is authenticated user or current user and second parameter is $subject, Since policies organize authorization logic around models.
Policies are classes that organize authorization logic around a
particular model or resource. For example, if your application is a
blog, you may have a Post model and a corresponding PostPolicy to
authorize user actions such as creating or updating posts.
if you want to go further deep inside it.
https://github.com/laravel/framework/blob/5.3/src/Illuminate/Auth/Access/Gate.php#L353
/**
* Resolve the callback for a policy check.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param string $ability
* #param array $arguments
* #return callable
*/
protected function resolvePolicyCallback($user, $ability, array $arguments)
{
return function () use ($user, $ability, $arguments) {
$instance = $this->getPolicyFor($arguments[0]);
// If we receive a non-null result from the before method, we will return it
// as the final result. This will allow developers to override the checks
// in the policy to return a result for all rules defined in the class.
if (method_exists($instance, 'before')) {
if (! is_null($result = $instance->before($user, $ability, ...$arguments))) {
return $result;
}
}
if (strpos($ability, '-') !== false) {
$ability = Str::camel($ability);
}
// If the first argument is a string, that means they are passing a class name
// to the policy. We will remove the first argument from this argument list
// because the policy already knows what type of models it can authorize.
if (isset($arguments[0]) && is_string($arguments[0])) {
array_shift($arguments);
}
if (! is_callable([$instance, $ability])) {
return false;
}
return $instance->{$ability}($user, ...$arguments);
};
}
See the last line where it is calling the method with $user and $argument( in our case Model ) is passed.
Laravel Docs for Authorization/Policies
It's possible to escape one or more policies methods using options parameter at authorizeResource with except:
public function __construct()
{
$this->authorizeResource(User::class, 'user', ['except' => ['view']]);
}
This should be on Laravel's documentation, but it isn't. I discovered this just guessing. I think this way it is a better approach thus, by removing authorizeResource method in the construct, it would be necessary to implement the authorization method for each resource action in order to protect the controller.

Accessing Lumen/Laravel URL params

I guess I'm missing something really obvious here but how do you access URL params in Lumen? I have the following route:
$app->get('user/{id}', ['uses' => 'userController#testId']);
Then in my user controller I have:
public function testId(Request $request) {
return $request->input('id');
}
But the ID is always null what have I missed even in this very basic example?
Okay, doesn't matter, as I thought I'm the tool here.
For anyone else (and myself in the future when I land on this page after a Google with the exact same problem next year) the documentation states:
If your controller method is also expecting input from a route parameter, simply list your route arguments after your other dependencies. For example, if your route is defined like so:
$app->put('user/{id}', 'UserController#update');
public function update(Request $request, $id)
{
//
}
So you need to pass any URL params into the function, they don't appear to be accessible via Lumens $request object.
public function testId(Request $request, $id) {
return $id;
}
According to https://laravel.com/docs/master/requests you can do the following:
$app->get('user/{id}', ['uses' => 'userController#testId']);
public function testId(Request $request, $id) {
//$id is from the path
}
The reasoning is that id is part of the request path and not the request input
Try with this
public function testId(Request $request) {
return $request->id;
}

Categories