Laravel Policy - Bug or Implementation error? - php

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');
}

Related

Laravel multiple policies always authenticated?

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

Laravel middleware limits access to unwanted functions

I am doing a project in Laravel.
I have a database with posts and users. These posts can be modified and edited by the user who created it and the admin.
To do this I created a new field for users, there is an admin and two editor.
After limiting the access with the middleware, only the admin and editor can access the posts.
$this->middleware('auth',['only' => ['create', 'store', 'edit', 'update', 'destroy']]);
$this->middleware(['auth', 'roles:admin'],['only' => ['edit', 'update', 'destroy']]);
The problem is that now only the admin can access the edit and delete post functions. Publishers are redirected to the home page.
Is there a way to put an if that bypasses the middleware redirect or something similar?
I'd use a policy to simplify things, and remove the middleware.
Create Policy
php artisan make:policy PostPolicy --model=Post
Resulting in this file
<?php
namespace App\Policies;
use App\Post;
use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class PostPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view any models.
*
* #param \App\User $user
* #return mixed
*/
public function viewAny(User $user)
{
//
}
/**
* Determine whether the user can view the model.
*
* #param \App\User $user
* #param \App\Post $post
* #return mixed
*/
public function view(User $user, Post $post)
{
//
}
/**
* Determine whether the user can create models.
*
* #param \App\User $user
* #return mixed
*/
public function create(User $user)
{
//
}
/**
* Determine whether the user can update the model.
*
* #param \App\User $user
* #param \App\Post $post
* #return mixed
*/
public function update(User $user, Post $post)
{
//
}
/**
* Determine whether the user can delete the model.
*
* #param \App\User $user
* #param \App\Post $post
* #return mixed
*/
public function delete(User $user, Post $post)
{
//
}
/**
* Determine whether the user can restore the model.
*
* #param \App\User $user
* #param \App\Post $post
* #return mixed
*/
public function restore(User $user, Post $post)
{
//
}
/**
* Determine whether the user can permanently delete the model.
*
* #param \App\User $user
* #param \App\Post $post
* #return mixed
*/
public function forceDelete(User $user, Post $post)
{
//
}
}
Modify the rules for each action, so for example we need to specify that only the admin or the post owner can update a post, so
public function update(User $user, Post $post)
{
if ($user->role === 'admin') {
return true;
}
return $post->user_id === $user->id;
}
And then register the policy
https://laravel.com/docs/8.x/authorization#registering-policies
<?php
namespace App\Providers;
use App\Models\Post;
use App\Policies\PostPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* #var array
*/
protected $policies = [
Post::class => PostPolicy::class,
];
/**
* Register any application authentication / authorization services.
*
* #return void
*/
public function boot()
{
$this->registerPolicies();
//
}
}
And finally authorize your controller, add this line to the constructor
public function __construct()
{
$this->authorizeResource(Post::class, 'post');
}
Note that the 2nd parameter in the function call is the name of the route parameter, post is going to be your route parameter if you created the a resourceful controller
If you are not using a resourceful controller, or want to authorize actions manually, then you can use without adding the above line in the constructor
https://laravel.com/docs/8.x/authorization#via-controller-helpers
public function update(Request $request, Post $post)
{
$this->authorize('update', $post);
// The current user can update the blog post...
}
the first parameter is the name of the policy method, and the 2nd paramter is the post object

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']
]);
}

laravel route and controller

i am a new laravel user and a have admin page which doing update delete and insert my problem is i dont know how to call this functions in route.
Note: all this options working on one page (admin).
so please can anyone help me ?!
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Requests;
use DB;
class BlogPostController extends Controller
{
/**
* Display a listing of the resource.
*
* #return Response
*/
public function index(){
$date = date('Y-m-d');
$time = time('H:i:s');
/*$sections = ['History' => 'history.png','Electronics' => 'electronics.png','Electrical' => 'electrical.png','Science' => 'science.png',
'Art'=>'ARt.png','Database'=>'database.png','Irrigation'=>'irrigation.png','Novel'=>'Novel.png','Style'=>'Stsyle.png'];
*/
$sections = DB ::table('sections')->get();
return view('libraryViewsContainer.library')->withSections($sections)->withDate($date)->withTime($time);
}
/**
* Show the form for creating a new resource.
*
* #return Response
*/
public function create(){
//return View::make('posts.create');
return '<center><h1>Creating new section in the library!</h1></center>';
}
/**
* Store a newly created resource in storage.
*
* #return Response
*/
public function store( Request $request){
$section_name = $request->input('section_name');
$file = $request->file('image');
$destinationPath = 'images';
$filename = $file->getClientOriginalName();
$file->move($destinationPath,$filename);
DB ::table('sections')->insert(['section_name'=>$section_name,'image_name'=>$filename]);
return redirect('admin');
}
/**
* Display the specified resource.
*
* #param int $id
* #return Response
*/
public function show($id){
// $post = Post::find($id);
// return View::make('posts.show')->with('post', $post);
}
/**
* Show the form for editing the specified resource.
*
* #param int $id
* #return Response
*/
public function edit($id){
// $post = Post::find($id);
// return View::make('posts.edit')->with('post', $post);
}
/**
* Update the specified resource in storage.
*
* #param int $id
* #return Response
*/
public function update($id,Request $request){
$section_name = $request->input('section_name');
DB ::table('sections')->where('id',$id)->update(['section_name'=>$section_name]);
return redirect('admin');
}
/**
* Remove the specified resource from storage.
*
* #param int $id
* #return Response
*/
public function destroy($id){
DB :: table('sections')->where('id',$id)->delete();
return redirect('admin');
}
public function admin()
{
$sections = DB ::table('sections')->get();
return view('libraryViewsContainer.admin',['sections'=>$sections]);
}
}
Not entirely sure of the question, but you list the routes in routes.php, under the web group (so it applies default checks).
When you have resources, they'll use CRUD operations (create, read, update, delete) and will correspond to the default operates in the class. Else, make your own function names, and put seperate routes.
Route::group(['middleware' => ['web', 'auth']], function () {
Route::resource('/user', 'UserController');
}
Another option is calling the method direct in your routes:
Route::get('/auth/login', '\App\Http\Controllers\Auth\AuthController#getLogin');
Route::any('/datafeed/{id}/validate', 'DataFeedController#validateQuery');
You'll notice {id} which is the variable available in the function you've selected i.e. function validateQuery($id);
Here's a full example:
class UserController extends BaseController
{
public function __construct(User $model)
{
parent::__construct($model);
}
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index()
{
$collection = $this->model->all();
return view('user.index')->with('collection', $collection);
}
/**
* Show the form for creating a new resource.
*
* #return \Illuminate\Http\Response
*/
public function create()
{
return view('user.create');
}
/**
* Store a newly created resource in storage.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$input = Input::except(['_method', '_token']);
$connector = $this->model->create($input);
return redirect()->action('UserController#show', [$connector->id]);
}
/**
* Display the specified resource.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function show($id)
{
$connector = $this->model->findOrFail($id);
return view('user.show')->with('connector', $connector);
}
/**
* Update the specified resource in storage.
*
* #param \Illuminate\Http\Request $request
* #param int $id
* #return \Illuminate\Http\Response
*/
public function edit($id)
{
$connector = $this->model->findOrFail($id);
return view('user.edit')->with('connector', $connector);
}
/**
* Show the form for editing the specified resource.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
$input = Input::except(['_method', '_token']);
$connector = $this->model->findOrFail($id);
$connector->update($input);
return redirect()->action('UserController#show', [$id]);
}
/**
* Remove the specified resource from storage.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function destroy($id)
{
$currentID = Auth::user();
$user = $this->model->findOrFail($id);
if ($currentID->id != $user->id) {
$user->delete();
} else {
Session::flash('flash_message', "You cant delete your own account!");
Session::flash('flash_type', 'alert-danger');
}
return redirect()->action('UserController#index');
}
And another example with a custom route:
Route::any('/datafeed/{id}/execute', 'DataFeedController#execute');
public function execute($id, $test = false) {
$results = $this->executeQuery($id, $test);
return view('datafeed.execute')->with('results', $results);
}
I'm not entirely sure on your plans (or even if you've fully read the documentation?) but you can access these functions by doing something similar to the following in your routes.php or Routes\web.php (depending on your version) file:
Route::get('/blog/create', 'BlogPostController#create');
Route::get('/blog/article/{id}', 'BlogPostController#show');
The first part is defining the route and the second part is saying what method should be run on the defined controller when this route is matched in the address bar. It doesn't always have to be get requests though, you can do get, post, patch and put

laravel how to handle profile pages?

I am playing around with Laravel 5. I am trying to build a site where a user can add some information about himself and it shows up in the frontend.
I am struggling to understand how to save the profile information only once.
Everytime the user call /profile/create a new DB entry is created. But I only need one profile entry per user!
If I don't provide a /profile/create route how can a user save his profile info to the DB? As the user can't call profile/edit because no entry exists.
This is my Controller:
namespace App\Http\Controllers;
use App\User;
use App\Profile;
use App\Http\Requests\ProfileRequest;
use App\Http\Requests;
use Illuminate\Http\Request;
use Auth;
class ProfilesController extends Controller
{
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index()
{
return view('backend.profile.index');
}
/**
* Show the form for creating a new resource.
*
* #return \Illuminate\Http\Response
*/
public function create()
{
return view('backend.profile.create');
}
/**
* Store a newly created resource in storage.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function store(ProfileRequest $request)
{
$profile = new Profile($request->all());
Auth::user()->profiles()->save($profile);
return 'saved';
}
/**
* Display the specified resource.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function show($id)
{
//
}
/**
* Show the form for editing the specified resource.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* #param \Illuminate\Http\Request $request
* #param int $id
* #return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function destroy($id)
{
//
}
}
My Profile Model:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Profile extends Model
{
protected $fillable = [
'name',
];
public function user() {
$this->belongsTo('App\User');
}
}
My User Model:
/**
* A User can have one Preference
* #return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function profiles() {
return $this->hasOne('App\Profile');
}
There are numerous ways, one is simply throw an exception if the user already has a profile:
public function store(ProfileRequest $request)
{
$user = Auth::user();
if($user->profiles){
abort(500);
}
$profile = new Profile($request->all());
$user()->profiles()->save($profile);
return 'saved';
}
In your template, you can show a link to either create profile or edit profile depending if the user has one yet.
You could do this check in middleware if you prefer, or make a guard.
Another nice method, as mentioned by #Maraboc in comments, is to create a blank profile on signup, so you only need an edit route.
Worth mentioning, if the user only has one profile, you should name the property 'profile' not 'profiles'
You may create empty profiles when creating new users. Another way is checkout if profile exists in your edit/update actions, like this:
public function edit()
{
//if profile will be edited at first time, then dummy profile will be used
$profile = Auth::user()->profile ?: new Profile();
return view('backend.profile.edit', compact('profile'));
}
public function update(Request $request)
{
//validate your data
//use $fillable in Profile model to whitelist acceptable attributes
if(Auth::user()->profile) {
Auth::user()->profile->update($request->all());
} else {
$profile = new Profile($request->all());
Auth::user()->profile()->save($profile);
}
//redirect to another page
}

Categories