Laravel multiple policies always authenticated? - php

I'm using Laravel 9 with the Laravel Spatie Permissions package. I have users and roles in my system. Users have roles, and depending on their permissions on their role they either can or can't create new users / new roles etc.
I've set up my UserPolicy and RolePolicy, and am passing my User model to each since it's the user that needs to be checked against what permissions they have, then in the controller of my choice, such as my RoleController I run:
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index()
{
$this->authorize('viewAny', User::class);
$roles = Role::with('permissions')->get();
if (!$roles || count($roles) <= 0) {
return response()->json([
'message' => 'No roles found'
], 404);
}
return response()->json([
'roles' => $roles
], 200);
}
Strangely, if I edit my RolePolicy's viewAny permission and return false, I'm still able to see the data? I shouldn't be. what am I missing?
Here's my RolePolicy
<?php
namespace App\Policies\UserManagement;
use Spatie\Permission\Models\Role;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class RolePolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view any models.
*
* #param \App\Models\User $user
* #return \Illuminate\Auth\Access\Response|bool
*/
public function viewAny(User $user)
{
// TODO: if I return false I still have access?
if ($user->can('role_index')) {
return true;
}
}
/**
* Determine whether the user can view the model.
*
* #param \App\Models\User $user
* #return \Illuminate\Auth\Access\Response|bool
*/
public function view(User $user)
{
if ($user->can('role_show')) {
return true;
}
}
/**
* Determine whether the user can create models.
*
* #param \App\Models\User $user
* #return \Illuminate\Auth\Access\Response|bool
*/
public function create(User $user)
{
if ($user->can('role_store')) {
return true;
}
}
/**
* Determine whether the user can update the model.
*
* #param \App\Models\User $user
* #return \Illuminate\Auth\Access\Response|bool
*/
public function update(User $user)
{
if ($user->can('role_update')) {
return true;
}
}
/**
* Determine whether the user can delete the model.
*
* #param \App\Models\User $user
* #return \Illuminate\Auth\Access\Response|bool
*/
public function delete(User $user)
{
if ($user->can('role_destroy')) {
return true;
}
}
}
And my AuthServiceProvider:
<?php
namespace App\Providers;
use App\Models\User;
use Spatie\Permission\Models\Role;
use App\Policies\UserManagement\UserPolicy;
use App\Policies\UserManagement\RolePolicy;
use Illuminate\Support\Facades\Gate;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* The model to policy mappings for the application.
*
* #var array<class-string, class-string>
*/
protected $policies = [
User::class => UserPolicy::class,
User::class => RolePolicy::class,
];
/**
* Register any authentication / authorization services.
*
* #return void
*/
public function boot()
{
$this->registerPolicies();
ResetPassword::createUrlUsing(function ($user, string $token) {
$frontendUrl = trim(rtrim(config('lespro.frontend_url'), '/'));
return $frontendUrl . '/account/reset/?email=' . $user->email . '&token=' . $token;
});
// Implicitly grant "super_admin" role all permissions
// This works in the app by using gate-related functions like auth()->user->can() and #can()
Gate::before(function ($user, $ability) {
return $user->hasRole('super_admin') ? true : null;
});
}
}

Related

Laravel Policy bug

I have used Laravel Policies successfully in the past but am having issues with one currently.
In an ArticleController I have the following method:
/**
* Show the form for creating a new resource.
*
* #return \Illuminate\Http\Response
*/
public function create()
{
$this->authorize('create', Article::class);
$categories = $this->categories;
return view('editable.news.create', compact('categories'));
}
My ArticlePolicy looks like this:
<?php
namespace App\Policies;
use Illuminate\Auth\Access\HandlesAuthorization;
use App\User;
use App\Article;
class ArticlePolicy
{
use HandlesAuthorization;
/**
* Create a new policy instance.
*
* #return void
*/
public function __construct()
{
//
}
/**
* Determine whether the user can view the post.
*
* #param \App\User $user
* #param \App\Post $post
* #return mixed
*/
public function show(User $user, Article $article)
{
// If the article is published
if ($article->published) {
return true;
}
// A user with permission can view unpublished articles
if ($user->can('view unpublished articles')) {
return true;
}
// Authors can view their own unpublished posts
if ($user->username === $article->author->username) {
return true;
}
}
/**
* Determine whether the user can create posts.
*
* #param \App\User $user
* #return mixed
*/
public function create(User $user)
{
return true;
}
/**
* Determine whether the user can update the post.
*
* #param \App\User $user
* #param \App\Post $post
* #return mixed
*/
public function update(User $user, Article $article)
{
if ($user->can('edit own articles')) {
return $user->username === $article->author->username;
}
if ($user->can('edit any articles')) {
return true;
}
}
/**
* Determine whether the user can delete the post.
*
* #param \App\User $user
* #param \App\Post $post
* #return mixed
*/
public function delete(User $user, Article $article)
{
// A user can delete their own articles
if ($user->can('delete own articles')) {
return $user->username === $article->author->username;
}
// A user with permission can delete any article
if ($user->can('delete any articles')) {
return true;
}
}
}
You can see in the create method I am just returning true, this is deliberate.
Whenever I hit the create blade I always receive a 403 error.
I also have an accompanying test:
/** #test */
public function a_user_with_permission_can_create_an_article()
{
$this->setupPermissions();
$user = factory(User::class)->create();
$user->assignRole('news contributor');
$article = factory(Article::class)->raw(['excerpt' => null]);
$this->actingAs($user)
->get(route('thanos.articles.create'))
->assertStatus(200);
$this->post(route('thanos.articles.store'), $article);
$this->assertDatabaseHas('articles', [
'user_username' => $user->username,
'title' => $article['title']
]);
}

How to override method in Laravel

I`m trying override "public function sendCode()" to use it in LoginController from below TokenModel.
have done more than tripled confirmed that loading and calling TokenModel from LoginController as an instance is succeed but "public function sendCode()" is not included with TokenModel.
would be very helpful if anyone knows what happens here and tell me what I should code.
=======================TokenModel=========================
<?php
namespace App;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
class Token extends Model
{
const EXPIRATION_TIME = 15; // minutes
protected $fillable = [
'code',
'user_id',
'used'
];
public function __construct(array $attributes = [])
{
if (! isset($attributes['code'])) {
$attributes['code'] = $this->generateCode();
}
parent::__construct($attributes);
}
/**
* Generate a six digits code
*
* #param int $codeLength
* #return string
*/
public function generateCode($codeLength = 4)
{
$min = pow(10, $codeLength);
$max = $min * 10 - 1;
$code = mt_rand($min, $max);
return $code;
}
/**
* User tokens relation
*
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
/**
* Send code to user
*
* #return bool
* #throws \Exception
*/
public function sendCode()
{
if (! $this->user) {
throw new \Exception("No user attached to this token.");
}
if (! $this->code) {
$this->code = $this->generateCode();
}
try {
app('twilio')->messages->create($this->user->getPhoneNumber(),
['from' => env('TWILIO_NUMBER'), 'body' => "Your verification code is {$this->code}"]);
} catch (\Exception $ex) {
return false; //enable to send SMS
}
return true;
}
/**
* True if the token is not used nor expired
*
* #return bool
*/
public function isValid()
{
return ! $this->isUsed() && ! $this->isExpired();
}
/**
* Is the current token used
*
* #return bool
*/
public function isUsed()
{
return $this->used;
}
/**
* Is the current token expired
*
* #return bool
*/
public function isExpired()
{
return $this->created_at->diffInMinutes(Carbon::now()) > static::EXPIRATION_TIME;
}
}
=======================LoginController=========================
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Token;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Lang;
class LoginController extends Controller
{
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
use AuthenticatesUsers;
/**
* Where to redirect users after login.
*
* #var string
*/
protected $redirectTo = '/home';
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
$this->middleware('guest', ['except' => 'logout']);
}
/**
* Handle a login request to the application.
*
* #param \Illuminate\Http\Request $request
* #return mixed
*/
public function login(Request $request)
{
$this->validateLogin($request);
//retrieveByCredentials
if ($user = app('auth')->getProvider()->retrieveByCredentials($request->only('email', 'password'))) {
$token = Token::create([
'user_id' => $user->id
]);
if ($token->sendCode()) {
session()->set("token_id", $token->id);
session()->set("user_id", $user->id);
session()->set("remember", $request->get('remember'));
return redirect("code");
}
$token->delete();// delete token because it can't be sent
return redirect('/login')->withErrors([
"Unable to send verification code"
]);
}
return redirect()->back()
->withInputs()
->withErrors([
$this->username() => Lang::get('auth.failed'),
]);
}
/**
* Show second factor form
*
* #return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View
*/
public function showCodeForm()
{
if (! session()->has("token_id")) {
return redirect("login");
}
return view("auth.code");
}
/**
* Store and verify user second factor.
*/
public function storeCodeForm(Request $request)
{
// throttle for too many attempts
if (! session()->has("token_id", "user_id")) {
return redirect("login");
}
$token = Token::find(session()->get("token_id"));
if (! $token ||
! $token->isValid() ||
$request->code !== $token->code ||
(int)session()->get("user_id") !== $token->user->id
) {
return redirect("code")->withErrors(["Invalid token"]);
}
$token->used = true;
$token->save();
$this->guard()->login($token->user, session()->get('remember', false));
session()->forget('token_id', 'user_id', 'remember');
return redirect('home');
}
}

Class App\Policies\StatusPolicy does not exist

I setup my classes so I can use Laravel Authorization and use the Policies feature. But I keep getting this error (Class App\Policies\StatusPolicy does not exist) when defining the middleware to my methods. This is what I have:
AuthServiceProvider.php
namespace App\Providers;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* #var array
*/
protected $policies = [
'App\Status' => 'App\Policies\StatusPolicy',
];
/**
* Register any authentication / authorization services.
*
* #return void
*/
public function boot()
{
$this->registerPolicies();
//
}
}
StatusController.php
namespace App\Http\Controllers;
use App\Status;
use Illuminate\Http\Request;
class StatusController extends Controller
{
public function __construct()
{
$this->middleware('can:list,App\Status')->only('index');
$this->middleware('can:update,status')->only('edit');
}
// ...
StatusPolicy.php (generated by php artisan make:policy StatusPolicy --model=Status
namespace Policies;
use App\User;
use App\Status;
use Illuminate\Auth\Access\HandlesAuthorization;
class StatusPolicy
{
use HandlesAuthorization;
/**
* Verifica se o usuário tem permissão para listar os status.
*
* #param \App\User $user
* #return bool
*/
public function list(User $user)
{
return true;
}
/**
* Determine whether the user can view the status.
*
* #param \App\User $user
* #param \App\Status $status
* #return mixed
*/
public function view(User $user, Status $status)
{
//
}
/**
* Determine whether the user can create statuses.
*
* #param \App\User $user
* #return mixed
*/
public function create(User $user)
{
//
}
/**
* Determine whether the user can update the status.
*
* #param \App\User $user
* #param \App\Status $status
* #return mixed
*/
public function update(User $user, Status $status)
{
return true;
}
/**
* Determine whether the user can delete the status.
*
* #param \App\User $user
* #param \App\Status $status
* #return mixed
*/
public function delete(User $user, Status $status)
{
//
}
}
I found the problem.
For some reason, the command php artisan make:policy created a file with a wrong namespace. The fix was to change the namespace in the StatusPolicy.php file:
From namespace Policies;
To namespace App\Policies;
change the namespace in your StatusPolicy class to App\Policies;

Laravel Policy - Bug or Implementation error?

I have implemented a user policy in Laravel as follows.
namespace App\Policies;
use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class UserPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can list the model.
*
* #param \App\User $user
* #return mixed
*/
public function index(User $user)
{
// only a chief editor can view all users
$authorized = false;
$authorized = ($user->role->name === 'Chief Editor');
return $authorized;
}
/**
* Determine whether the user can view the model.
*
* #param \App\User $user
* #param \App\User $model
* #return mixed
*/
public function view(User $user, User $model)
{
// only a chief editor or user(who owns the user) can view the user
$authorized = false;
$authorized = ($user->role->name === 'Chief Editor' || $user->id === $model->id);
return $authorized;
}
/**
* Determine whether the user can create models.
*
* #param \App\User $user
* #return mixed
*/
public function create(User $user)
{
// only a chief editor can create a user
$authorized = false;
$authorized = ($user->role->name === 'Chief Editor');
return $authorized;
}
/**
* Determine whether the user can update the model.
*
* #param \App\User $user
* #param \App\User $model
* #return mixed
*/
public function update(User $user, User $model)
{
// only a chief editor or user(who owns the user) can update the user
$authorized = false;
$authorized = ($user->role->name === 'Chief Editor' || $user->id === $model->id);
return $authorized;
}
/**
* Determine whether the user can delete the model.
*
* #param \App\User $user
* #param \App\User $model
* #return mixed
*/
public function delete(User $user, User $model)
{
// only a chief editor or user(who owns the user) can delete the user
$authorized = false;
$authorized = ($user->role->name === 'Chief Editor' || $user->id === $model->id);
return $authorized;
}
}
And here is my user controller.
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests\StoreUser;
use App\Http\Requests\UpdateUser;
use App\User;
use App\Role;
class UserController extends Controller
{
public function __construct()
{
$this->middleware('auth');
$this->middleware('can:index,App\User')->only('index');
$this->middleware('can:view,user')->only('show');
$this->middleware('can:create,App\User')->only('create', 'store');
$this->middleware('can:update,user')->only('edit', 'update');
$this->middleware('can:delete,user')->only('destroy');
}
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index()
{
$users = User::paginate(5);
return view('users.index')
->with('users', $users);
}
/**
* Show the form for creating a new resource.
*
* #return \Illuminate\Http\Response
*/
public function create()
{
// fetch roles
$roles = Role::all();
return view('users.create')
->with('roles', $roles);
}
/**
* Store a newly created resource in storage.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function store(StoreUser $request)
{
//
}
/**
* Display the specified resource.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function show($id)
{
$user = User::findOrFail($id);
return view('users.show')
->with('user', $user);
}
/**
* Show the form for editing the specified resource.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function edit($id)
{
$user = User::findOrFail($id);
return view('users.edit')
->with('user', $user);
}
/**
* Update the specified resource in storage.
*
* #param \Illuminate\Http\Request $request
* #param int $id
* #return \Illuminate\Http\Response
*/
public function update(UpdateUser $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function destroy($id)
{
//
}
}
However, when I access an action with multiple parameters in the policy method(two user objects), I get the This action is unauthorized. error.
I have also tried returning true without any checks from these methods but still the same issue persists.
Is this my code issue or a bug with Laravel?
Update: Your code is not working because you'r not passing the second user to the policy.
You'r not using route model binding, so the user passed to the view method, for example, does not exist. When the model does not exist, Laravel will not check for the policy and just return false.
The following is an example from the documentation, and it works becuase of the route model binding.
Route::put('/post/{post}', function (Post $post) {
// The current user may update the post...
})->middleware('can:update,post');
The Answer:
Does not make sense to use authorization in the controller constructor. If you want to use middleware for authorization, attach it to the route not the controller.
If you allow the request to reach the controller, then it would be better to check for authorization in the action not the constructor. It will also be easier to read and debug.
I would remove all the calls to can middleware from the constructor and replace it with a call to authorize in the action.
You controller should be like this:
public function __construct()
{
$this->middleware('auth');
}
public function index()
{
$this-authorize('index'); // This will check for authorization
$users = User::paginate(5);
return view('users.index')
->with('users', $users);
}
public function show($id)
{
$user = User::findOrFail($id);
$this->authorize('view', $user);
return view('users.show')
->with('user', $user);
}
Also, no need for the temprory variable in your policy class.
public function index(User $user)
{
// only a chief editor can view all users
return $user->role->name === 'Chief Editor'
}
public function view(User $user, User $model)
{
// only a chief editor or user(who owns the user) can view the user
return $user->role->name === 'Chief Editor' || $user->id === $model->id)
}
As a side note, in your show action, Why don't you use implicit route model binding? It would be much cleaner.
// web.php
Route::get('users/{user}', 'UserController#show');
// UserController.php
public function show(User $user)
{
$this->authorize('view', $user);
return view('users.show', compact('user');
}

Laravel index policy

I using Laravel 5.4 and I am trying to write a policy for my index view. I am trying to use a Method Without a Model, I am receiving the following error:
HttpException in Handler.php line 133:
This action is unauthorized.
Here is my Controller:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\County;
use Session;
use App\Http\Controllers\Controller;
class CountyController extends Controller
{
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
$this->middleware('auth');
}
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index()
{
$counties = County::orderBy('id', 'desc')->paginate(5);
$this->authorize('index');
return view('county.index', array(
'counties' => $counties
));
}
Here is my AuthServicePovider:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use App\Role;
use App\County;
use App\Policies\CountyPolicy;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* #var array
*/
protected $policies = [
County::class => CountyPolicy::class,
];
/**
* Register any authentication / authorization services.
*
* #return void
*/
public function boot()
{
$this->registerPolicies();
Gate::define('is-Admin', function ($user) {
if($user->roles()->where('name','Admin')->first()){
return true;
}
return false;
});
}
}
Here is my Policy:
<?php
namespace App\Policies;
use App\User;
use App\Role;
use App\County;
use Illuminate\Auth\Access\HandlesAuthorization;
class CountyPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view the county.
*
* #param \App\User $user
* #param \App\County $county
* #return mixed
*/
public function index(User $user)
{
$userRoles = $user->getRoleNames();
$acceptedRoles = ['Sudo','Admin'];
$testArr = array_intersect($acceptedRoles, $userRoles);
dd($testArr);
if(!empty($testArr)){
return true;
}
return false;
//
}
/**
* Determine whether the user can view the county.
*
* #param \App\User $user
* #param \App\County $county
* #return mixed
*/
public function view(User $user, County $county)
{
$userRoles = $user->getRoleNames();
$acceptedRoles = ['Sudo','Admin','Client'];
$testArr = array_intersect($acceptedRoles, $userRoles);
if(!empty($testArr)){
return true;
}
return false;
//
}
/**
* Determine whether the user can create counties.
*
* #param \App\User $user
* #return mixed
*/
public function create(User $user)
{
//
}
/**
* Determine whether the user can update the county.
*
* #param \App\User $user
* #param \App\County $county
* #return mixed
*/
public function update(User $user, County $county)
{
//
}
/**
* Determine whether the user can delete the county.
*
* #param \App\User $user
* #param \App\County $county
* #return mixed
*/
public function delete(User $user, County $county)
{
//
}
}
I never get to dd($testArr) in the index policy. Also the view policy is working perfectly.
How do I write a policy for my index view?
keeping everything the same but changing:
$this->authorize('index');
to
$this->authorize('index', County::class);
fixed the problem. Apparently the model class needs to be passed on actions that don't require a model. This is only described under the middleware section of Laravel's docs, not the controller helpers... A little confusing.

Categories