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.
Related
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
}
Building an app (Blog/posts).
Where only auth users can edit their post(which ofcourse belongs to them only).
For example, Post with an id of 15 belongs to particular user, so if he edits it, the route will be like this
http://localhost:8000/post/15/edit
this is correct.
But when the user enters any other post ID(which doesn't belongs to him) in the route, it shows
http://localhost:8000/post/16/edit
ErrorException (E_NOTICE)
Trying to get property 'user_id' of non-object
How to show unauthorised page in this case?
This is the postController
public function edit($id)
{
$post = Post::find($id);
if(Auth::user()->id == $post->user_id){
return view('post-edit',compact('post'));
}else {
return redirect()->route('home');
}
}
The following code checks if the post exist (which is why you are getting the error Trying to get property 'user_id' of non-object, because it doesn't exist), and then checks if it belongs to the user in the same condition. If it's not valid it aborts with a 403 UNAUTHORIZED error code.
public function edit($id)
{
$post = Post::find($id);
if (empty($post) || Auth::id() != $post->user_id) {
abort(403);
}
else {
return view('post-edit',compact('post'));
}
}
Here is a better version that checks if a post exist, with the specified ID, but also with the right user and throws an exception otherwise:
public function edit($id)
{
$post = Post::whereHas('user', function ($q) {
$q->where('users.id', Auth::id());
})->findOrFail($id);
return view('post-edit',compact('post'));
}
A third version, on the same idea as the 2nd one, but simpler:
public function edit($id)
{
$post = Post::where('user_id', Auth::id())->findOrFail($id);
return view('post-edit',compact('post'));
}
use laravel authorization policy to authorize users.
php artisan make:policy PostPolicy --model=Post
This command will create PostPolicy.php in app\policies dir.
now you'll have to register the policy in AuthServiceProvider. So first add use statements of your policy and model for example.
use App\Post;
use App\Policies\PostPolicy;
then find protected $policies and in that array register your policy. Model followed by policy.
protected $policies = [
Post::class => PostPolicy::class,
];
Now in your Policy that we generated using artisan command. will hold all CRUD related methods. each of them accepts two parameters one is User and second is the model you want to authorize except create method. note that you can modify create or other methods to accept more parameters. it's upto you.
Now for example in your policy let's build logic for update method.
/**
* 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;
}
As you can see return Boolean here. you can customize methods as you want. Next in your controller method. where you want to authorize user simply add
public function update(Post $post)
{
$this->authorize('update', $post);
// then your logic here.
}
For create authorization you just pass pass empty class
$this->authorize('create', Post::class);
It accepts two parameters one is authorization method name and second is model.It automatically get's authenticated user and authorize user. if not authorized then throws Illuminate\Auth\Access\AuthorizationException which is 403.
Also if you need to modify the 403 error view you'll need to create 403 blade in
resources/views/errors/403.blade.php
Everything is well documented in laravel doc.
Extra tip if you are going to use some Boolean datatype value for returned from database as tinyint which are 1 or 0. for example
public function view(User $user, Post $post)
{
if(! $post->isPrivate) {
return true;
}
return $user->id === $post->user_id;
}
then make sure to cast that value to Boolean in model to return as true or false. because it was not working for when i deployed my application on shared hosting. Later i found that it was returning as a string. also the version of the database was old.
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);
}
I am not sure if I am using this correctly, but I am utilising the requests in Laravel 5, to check if the user is logged in and if he is the owner of an object. To do this I need to get the actual object in the request class, but then I need to get the same object in the controller?
So instead of fetching it twice, I thought, why not just set the object as a variable on the request class, making it accessible to the controller?
It works, but I feel dirty? Is there a more appropriate way to handle this?
Ex.
Request Class
class DeleteCommentRequest extends Request {
var $comment = null;
public function authorize() {
$this->comment = comment::find(Input::get('comment_id'));
$user = Auth::user();
if($this->comment->user == $user)
return true;
return false;
}
public function rules() {
return [
'comment_id' => 'required|exists:recipes_comments,id'
];
}
}
Ex. Controller:
public function postDeleteComment(DeleteCommentRequest $request) {
$comment = $request->comment;
$comment->delete();
return $comment;
}
So what is my question? How do I best handle having to use the object twice when using the new Laravel 5 requests? Am I possibly overextending the functionality of the application? Is it ok to store the object in the application class so I can reach it later in my controller?
I would require ownership on the query itself and then check if the collection is empty.
class DeleteCommentRequest extends Request {
var $comment = null;
public function authorize() {
$this->comment = comment::where('id',Input::get('comment_id'))->where('user_id',Auth::id())->first();
if($this->comment->is_empty())
return false;
return true;
}
public function rules() {
return [
'comment_id' => 'required|exists:recipes_comments,id'
];
}
}
Since you're wanting to use the Model in two different places, but only query it once I would recommenced you use route-model binding.
In your RouteServiceProvider class (or any relevant provider) you'll want to bind the comment query from inside the boot method. The first parameter of bind() will be value that matches the wildcard in your route.
public function boot()
{
app()->router->bind( 'comment_id', function ($comment_id) {
return comment::where('id',$comment_id)->where('user_id',Auth::id())->first();
} );
}
Once that's set up you can access the Model from your DeleteCommentRequest like so
$this->comment_id
Note: The variable is Comment_id because that's what matches your route, but it will contain the actual model.
From your controller you just inject it like so
public function postDeleteComment(Comment $comment, DeleteCommentRequest $request) {
$comment->delete();
return $comment;
}
This is my child controller:
class VolunteersController extends \BaseController
{
public function index()
{
$this->checkForRoles(['admin']);
//list some secret stuff for admin
}
}
In my base controller I did this:
class BaseController extends Controller
{
protected function checkForRoles($roles)
{
foreach ($roles as $role) {
if (!(Auth::user()->hasRole($role))) {
return Redirect::to('/');
}
}
}
}
Now what I expected was that the line return Redirect::to('/'); in BaseController would redirect the user to home page if his role is not admin.
But it does not happen. //list some secret stuff for admin gets executed in any case.
Edit:
Some people may wonder, why am I not using filters. Well yeah, the desired functionality is of filters but apparently filters do not support array arguments in Laravel yet. And as you can see, I need to pass an array of roles to the function.
Please help.
The redirect will happen only if VolunteersController::index() will return a "Redirect". It does not do so in your code.
It would, if you had
class VolunteersController extends \BaseController
{
public function index()
{
if ($res = $this->checkForRoles(['admin'])) return $res;
//list some secret stuff for admin
}
}
I would move the logic to a filter, which will allow the Redirect to function properly. This is the kind of thing filters were designed for.
If you need to pass multiple roles to the filter, instead of passing an array to the filter (which Laravel won't allow), use a delimiter like "+" and then explode the parameter in the filter to simulate passing an array.
For example, your route would be:
Route::get('volunteer', array(
'before' => 'roles:admin+author',
'uses' => 'VolunteersController#index'
));
...and then your filter can easily convert the multiple roles into an array:
Route::filter('roles', function($route, $request, $roles)
{
$roles = explode('+', $roles);
// 'admin+author' becomes ['admin', 'author'];
// continue with your checkForRoles function from above:
foreach ($roles as $role) {
if (!(Auth::user()->hasRole($role))) {
return Redirect::to('/');
}
}
}
Then you can remove the logic from the BaseController.
Alternatively, you can pass multiple parameters to a filter as a comma-delimited list. So if you called your route with 'before' => 'roles:admin,author', you could access them in your filter using func_get_args():
Route::filter('roles', function($route, $request, $roles)
{
$roles = array_slice(func_get_args(), 2); // remove $route and $request
//...continue as above.