Laravel Policies: Is there a bug? a cache? how to solve? - php

I see the very same problem here: Laravel policies : code change is ignored. Is there any policy cache to clear? and here: Laravel Policy bug
I'm writing a new policy, the easiest one, over mode User to check if the logged user is the same user in the database so he can edit his profile, so...
I create the policy file:
> php artisan make:policy UserPolicy
I register the policy in AuthServiceProvider.php:
...
protected $policies = [
// 'App\Model' => 'App\Policies\ModelPolicy',
User::class => UserPolicy::class,
];
...
In UserPolicy.php I create the edit function:
public function edit(User $authUser, User $user) {
return $authUser->id === $user->id;
}
In UserController.php I have edit:
public function edit($id)
{
//
$user = User::findOrFail($id);
$this->authorize($user);
return view('user.edit', compact('user'));
}
See somwthing wrong? Me neither, because it worked... the first time. Then I wanted to change the policy, the User model has a level attribute, 1 for normal users, 5 for admins, 99 for superuser and so on. So I wanted that the admins or superuser would be able to change the user data, so I rewrote the UserPolicy.php's editfunction as:
public function edit(User $authUser, User $user) {
return ($authUser->id === $user->id) || ($user->level > 1);
}
Of course I made a mistake here, I should've checked for $authUser and nor $user. When I checked in the browsser, function returned false, and server gave me a 403
This action is unauthorized., which is okay. Now the wierd thing. I correct the fuction:
public function edit(User $authUser, User $user) {
return ($authUser->id === $user->id) || ($authUser->level > 1);
}
it returns 403...
public function edit(User $authUser, User $user) {
return true;
}
It returns 403...
I delete the function from the file... It returns 403...
I delete the register from AuthServiceProvider... Ir returns 403...
No, I'm not using Gates, or some other thing, the Laravel app is almost virgin. I have had this problem in the past, that came out of the blue, and went the same way as it came. I have no idea where to look for, what to look for... I thought that would be some interaction that I didn't grasped, so I wanted to start with the policies from the beggining of this project.
EDIT::::::::::::::::::::::::::::
This is even more absurd... If I check using can() in tinker, this /#&)"# thing does what it is supposed to do:
> php artisan tinker
>>> $user = App\User::find(1)
>>> $user->can('edit', $user)
true
>>> $user2 = App\User::find(2)
>>> $user->can('edit', $user2)
false
So, problem is here????
$this->authorize($user);
EDIT 2 ::::::::::::: SOLVED ::::::::::::::::::::::
I swear this used to work as I posted above (at least it used to work in 5). I had to change
$this->authorize($user);
for
$this->authorize('edit', $user);
Solution came from this article

I swear this used to work as I posted above (at least it used to work in 5). I had to change
$this->authorize($user);
for
$this->authorize('edit', $user);
Solution came from this article

Related

Can't get Laravel middleware "can:something" to work with my policy

I want to make sure that the current user is able to edit the users credentials so I made the following UserPolicy:
class UserPolicy
{
use HandlesAuthorization;
public function update(User $user, User $model)
{
return true;
//return $user->is($model);
}
}
I even registered the policiy inside AppServiceProvider:
protected $policies = [
User::class => UserPolicy::class
];
Now I try to add the following middleware to the update-route in web.php: "->middleware('can:update,user');" like this:
Route::patch('/profiles/{user}',function (){
dd('It works');
})->middleware('can:update,user');
But I keep getting the following error:
Error Class '2' not found
Where 2 is the user-id who we try to patch. If I was logged in with user-id 1 that will be the class not found. I don't understand why. I followed the documentation on Laravel website (https://laravel.com/docs/8.x/authorization#via-middleware).
I have also tried to set {user} to {user:id} -> Same result
I have tried adding the id on "can" like this: can:update,user:id -> Gives 403 not authorized
The edit.blade.php has the following:
<form action="/profiles/{{ auth()->user()->id }}" method="POST">
#csrf
#method('PATCH')
...INPUTS...
</form>
I have of course tried running: "php artisan optimize" with no effect
What am I missing here? What's wrong?
EDIT:
I now tried the same thing with a Gate instead. I put the following inside AppServiceProvider.php:
public function boot()
{
Gate::define('edit-user', function(User $currentUser, User $user){
return true;
//return $currentUser->id === $user->id;
});
}
And the following middleware inside web.php:
Route::patch('/profiles/{user}',function (){
dd('It works');
})->middleware('can:edit-user,user');
And it gives me the exact same error: Class 2 not found
I even tried to pass the full models path like this:
Route::patch('/profiles/{user}',function (){
dd('It works');
})->middleware('can:edit-user,App\Models\User');
And it gives me the following error:
Argument 2 passed to
App\Providers\AppServiceProvider::App\Providers{closure}() must be an
instance of App\Models\User, string given, called in
/var/www/vhosts/domain.com/httpdocs/vendor/laravel/framework/src/Illuminate/Auth/Access/Gate.php
on line 474
I would think it is due to not using model binding, you are passing the id where it expects an user model. Check if this version works.
Route::patch('/profiles/{user}',function (User $user) {
dd('It works');
})->middleware('can:edit-user,user');

Laravel Socialite doesn't work on Google Chrome

I have one problem with Laravel Socialite login, in my Chrome works normally but in rest of the people browser doesn't work (works in other browsers). Before php update in server to 7.3.18 from 7.1 and update to Laravel 6 from 5.8, all works normally. I try to clear all caches, change session mode to cookie(file before), clear session and cookies in browser but nothing solved the problem.
When try to login, give me this
And this is my code:
public function loginSocial(Request $request){
$this->validate($request, [
'social_type' => 'required|in:google,facebook'
]);
$socialType = $request->get('social_type');
return Socialite::driver($socialType)->stateless()->redirect();
}
public function loginCallback(Request $request){
$socialType = $request->session()->get('social_type');
//Aparently, this get give to $socialType null in ppl browser. I dont understand why this get doesn't works.
$userSocial = Socialite::driver($socialType)->stateless()->user();
//If use 'google' instead $socialType, works fine.
$user = User::where('email',$userSocial->email)->first();
\Auth::login($user);
return redirect()->intended($this->redirectPath());
}
I understand what you are trying to do but sometimes less is more and more is less..... the call back is being made by the provider and not the user. anyway have different methods for each social login
// Google login
public function googleSocialLogin(Request $request){
Socialite::driver('google')->stateless()->redirect();
}
// Google callback
public function googleSocialLoginCallback(){
$userSocial = Socialite::driver('google')->stateless()->user();
$user = User::where('email',$userSocial->email)->first();
\Auth::login($user);
return redirect()->intended($this->redirectPath());
}
// Facebook login
public function facebookSocialLogin(Request $request){
Socialite::driver('facebook')->stateless()->redirect();
}
// Facebook callback
public function facebookSocialLoginCallback(){
$userSocial = Socialite::driver('facebook')->stateless()->user();
$user = User::where('email',$userSocial->email)->first();
\Auth::login($user);
return redirect()->intended($this->redirectPath());
}
With your methods separated you will have different routes for different social login which IMO is far better as they are have slightly different return params and you may want to perform additional function for a particular social login in future.

Laravel Authorization doesn't work

I'm trying to hide a button and show it only to the admin, I think i writed the code well but still can't get the result i want, i can't see the button with the admin account neither with a normal user account
The FolderPolicy code
public function create(User $user)
{
if($user->is_admin)
return true;
}
the view
#can('create')
<a class="btn btn-primary" href="{{ route('newdoss') }}">New Folder</a>
#endcan
the AuthServiceProvider file
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
'App\Folder' => 'App\Policies\FolderPolicy',
];
i know there is other ways to get the result i want by checking direclty if the authenticated user is an admin, but i want to figure out why this one is not working. Thanks.
You need to specify which resource, or model, you are trying to create. With create, you can pass the name of the class. For updates and deletes, you can pass the instance of the class.
#can('create', \App\Folder::class)
You also need to return false when the policy fails. You can simplify your create method to:
public function create(User $user)
{
return (bool) $user->is_admin
}

Testing unauthorized user restriction in Laravel PHPUnit

Laravel Version 5.2
In my project, users with role_id = 4 has the admin role and can manage users.
I have defined the following ability in AuthServiceProvider:
public function boot(GateContract $gate)
{
$this->registerPolicies($gate);
$gate->define('can-manage-users', function ($user)
{
return $user->role_id == 4;
});
}
I have used this ability in the UserController __construct method as follows:
public function __construct()
{
$this->authorize('can-manage-users');
}
In ExampleTest, I have created two tests to check if the defined authorization works.
The first test for admin user who has role_id = 4. This test passes.
public function testAdminCanManageUsers()
{
$user = Auth::loginUsingId(1);
$this->actingAs($user)
->visit('users')
->assertResponseOk();
}
The second test is for another user who does not have role_id = 4. I have tried with response status 401 and 403. But the test is failing:
public function testNonAdminCannotManageUsers()
{
$user = Auth::loginUsingId(4);
$this->actingAs($user)
->visit('users')
->assertResponseStatus(403);
}
First few lines of the failure message is given below:
A request to [http://localhost/users] failed. Received status code [403].
C:\wamp\www\laravel\blog\vendor\laravel\framework\src\Illuminate\Foundation\Testing\Concerns\InteractsWithPages.php:196
C:\wamp\www\laravel\blog\vendor\laravel\framework\src\Illuminate\Foundation\Testing\Concerns\InteractsWithPages.php:80
C:\wamp\www\laravel\blog\vendor\laravel\framework\src\Illuminate\Foundation\Testing\Concerns\InteractsWithPages.php:61
C:\wamp\www\laravel\blog\tests\ExampleTest.php:33
Caused by exception 'Illuminate\Auth\Access\AuthorizationException'
with message 'This action is unauthorized.' in
C:\wamp\www\laravel\blog\vendor\laravel\framework\src\Illuminate\Auth\Access\HandlesAuthorization.php:28
I have also tried to use 'see' method as follows:
public function testNonAdminCannotManageUsers()
{
$user = Auth::loginUsingId(4);
$this->actingAs($user)
->visit('users')
->see('This action is unauthorized.');
}
But it's failing too. What am I doing wrong? How can I make the test pass?
The mistake is calling the visit method. The visit method is in the InteractsWithPages trait. This method calls the makeRequest method which in turn calls assertPageLoaded method. This method gets the status code returned and if it gets code other than 200, it catches a PHPUnitException and throws an HttpException with the message
"A request to [{$uri}] failed. Received status code [{$status}]."
This is why the test was failing with the above message.
The test can be successfully passed by using get method instead of visit method. For example:
public function testNonAdminCannotManageUsers()
{
$user = App\User::where('role_id', '<>', 4)->first();
$this->actingAs($user)
->get('users')
->assertResponseStatus(403);
}
This test will pass and confirm that a non admin user cannot access the url.
Since the Auth middleware redirects to a login route when unauthenticated by default you could also perform the following test:
public function testNonAdminCannotManageUsers()
{
$user = Auth::loginUsingId(4);
$this->actingAs($user)
->visit('users')
->assertRedirect('login');
}
Since at least Laravel 5.4, you'll want to use the assertStatus(403) method.
public function testNonAdminCannotManageUsers()
{
$user = Auth::loginUsingId(4);
$this->actingAs($user)
->visit('users')
->assertStatus(403);
}

Laravel running tests as different users

I am using Laravel 5.1 and I am trying to test my controllers.
I have several roles for my users and policies defined for different actions. Firstly, each of the requests needs to be made by an authenticated user, so running a test with no user returns a 401 Unauthorized, as expected.
But when I want to test the functionality for authorized users, I still get the 401 Unauthorized status code.
It may be worth mentioning that I use basic stateless HTTP authentication on these controllers.
I have tried the following:
public function testViewAllUsersAsAdmin()
{
$user = UserRepositoryTest::createTestAdmin();
Auth::login($user);
$response = $this->call('GET', route('users.index'));
$this->assertEquals($response->getStatusCode(), Response::HTTP_OK);
}
and
public function testViewAllUsersAsAdmin()
{
$user = UserRepositoryTest::createTestAdmin();
$response = $this->actingAs($user)
->call('GET', route('users.index'));
$this->assertEquals($response->getStatusCode(), Response::HTTP_OK);
}
and also this (in case there was anything wrong with my new user, which there shouldn't be)
public function testViewAllUsersAsAdmin()
{
$user = User::find(1);
$response = $this->actingAs($user)
->call('GET', route('users.index'));
$this->assertEquals($response->getStatusCode(), Response::HTTP_OK);
}
but in every case I get a 401 response code so my tests fail.
I can access the routes fine using postman when logging in as a dummy user.
I am running out of ideas, so any help would be appreciated.
You need to add Session::start() in the setUp function or in the beginning of the function which user need to log in.
public function setUp()
{
parent::setUp();
Session::start();
}
or
public function testViewAllUsersAsAdmin()
{
Session::start();
$user = UserRepositoryTest::createTestAdmin();
Auth::login($user);
$response = $this->call('GET', route('users.index'));
$this->assertEquals(Response::HTTP_OK, $response->getStatusCode());
}
Through some experimentation, I found that the problem lay inside my authentication middleware. Since I want the API to be stateless, the authentication looks like this:
public function handle($request, Closure $next)
{
return Auth::onceBasic() ?: $next($request);
}
And apparently, it's not possible to authenticate a user the way I was doing it.
My solution was simply to disable the middleware, using the WithoutMiddleware trait or $this->withoutMiddleware() at the beginning of each test.

Categories