Laravel middleware limits access to unwanted functions - php

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

Related

PHP/Laravel - Extending authorizeResource to work on custom method

I have a resource controller called StreamController.php, that utilizes a policy called StreamPolicy.php.
In my controller, I have this:
//StreamController.php
/**
* Construct method.
*/
public function __construct()
{
$this->middleware('auth');
$this->authorizeResource(Stream::class, 'stream');
}
With above, all the RESTful endpoints is successfully "protected" using the policy.
However, I have added a new method to my controller, called documents(), like so:
//web.php
Route::get('streams/{stream}/documents', 'StreamController#documents');
//StreamController.php
/**
* Display the imported documents of the resource
*
* #return \Illuminate\Http\Response
*/
public function documents(Stream $stream)
{
return view('streams.documents', compact('stream'));
}
Now the problem is if I visit the URL:
example.com/streams/1 and I am not the owner of the stream, I get a 403 page - but if I go to:
example.com/streams/1/documents and I am not the owner of the stream, I can still access the page.
What am I doing wrong? How can I make so my policy also covers the documents() methods in my controller?
Edit:
This is my StreamPolicy.php file:
//StreamPolicy.php
namespace App\Policies;
use App\User;
use App\Stream;
use Illuminate\Auth\Access\HandlesAuthorization;
class StreamPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view the stream.
*
* #param \App\User $user
* #param \App\Stream $stream
* #return mixed
*/
public function view(User $user, Stream $stream)
{
return $user->id == $stream->user_id;
}
/**
* Determine whether the user can create streams.
*
* #param \App\User $user
* #return mixed
*/
public function create(User $user)
{
//
return true;
}
/**
* Determine whether the user can update the stream.
*
* #param \App\User $user
* #param \App\Stream $stream
* #return mixed
*/
public function update(User $user, Stream $stream)
{
//
return $user->id == $stream->user_id;
}
/**
* Determine whether the user can delete the stream.
*
* #param \App\User $user
* #param \App\Stream $stream
* #return mixed
*/
public function delete(User $user, Stream $stream)
{
//
return $user->id == $stream->user_id;
}
/**
* Determine whether the user can restore the stream.
*
* #param \App\User $user
* #param \App\Stream $stream
* #return mixed
*/
public function restore(User $user, Stream $stream)
{
//
}
/**
* Determine whether the user can permanently delete the stream.
*
* #param \App\User $user
* #param \App\Stream $stream
* #return mixed
*/
public function forceDelete(User $user, Stream $stream)
{
//
}
}
Controller.php uses "AuthorizesRequest" trait which defines below 2 methods:
trait AuthorizesRequests
{
/**
* Get the map of resource methods to ability names.
*
* #return array
*/
protected function resourceAbilityMap()
{
return [
'show' => 'view',
'create' => 'create',
'store' => 'create',
'edit' => 'update',
'update' => 'update',
'destroy' => 'delete',
];
}
/**
* Get the list of resource methods which do not have model parameters.
*
* #return array
*/
protected function resourceMethodsWithoutModels()
{
return ['index', 'create', 'store'];
}
You can override these 2 protected methods per controller basis because every controller extends Controller.php
class UserController extends Controller
{
public function __construct ()
{
$this->authorizeResource ( User::class, 'user' );
}
/**
* Get the map of resource methods to ability names.
*
* #return array
*/
protected function resourceAbilityMap()
{
return [
'show' => 'view',
'create' => 'create',
'store' => 'create',
'edit' => 'update',
'update' => 'update',
'destroy' => 'delete',
'customMethod'=>'customMethod',
'customMethodWithoutModel'=>'customMethodWithoutModel'
];
}
/**
* Get the list of resource methods which do not have model parameters.
*
* #return array
*/
protected function resourceMethodsWithoutModels()
{
return ['index', 'create', 'store','customMethodWithoutModel'];
}
Its Policy Class
class UserPolicy
{
/**
* Determine whether the user can custom method.
*
* #param \App\User $user
* #param \App\User $model
* #return mixed
*/
public function customMethod(User $user, User $model){
return true;
}
/**
* Determine whether the user can custom method without model.
*
* #param \App\User $user
* #return mixed
*/
public function customMethodWithoutModel(User $user){
return true;
}
I don't know exactly why is not working but I'm afraid that the authorizeResource method only handles the routes for the well-known resources end-points: view, create, update, delete and restore.
Later edit: Take a look in the docs to see which are the actions handled by the Resource Controllers https://laravel.com/docs/5.7/controllers#resource-controllers
What you should do is to explicitly set the authorization to the new route:
Route::get('streams/{stream}/documents', 'StreamController#documents')->middleware('can:documents,stream');
Of course, the documents method should exist on the StreamPolicy class.
OR
To authorize inside the StreamController.documents method:
public function documents(Stream $stream)
{
$this->authorize('documents', $stream);
return view('streams.documents', compact('stream'));
}

Custom authentication through API

I have a laravel API project. I want to be able to send a login request and get back a token depending on some custom logic. I am not using a database so i cant use the default auth.
I have set up a provider called AuthCustomProvider.
namespace App\Providers;
use Auth;
use App\Authentication\UserProvider;
use Illuminate\Support\ServiceProvider;
class AuthCustomProvider extends ServiceProvider
{
/**
* Perform post-registration booting of services.
*
* #return void
*/
public function boot()
{
Auth::provider('custom_auth', function($app, array $config) {
return new UserProvider();
});
}
/**
* Register bindings in the container.
*
* #return void
*/
public function register()
{
//
}
}
I have then added this to the config/app.php file in the providers array:
'providers' => [
App\Providers\AuthCustomProvider::class,
Then i added my custom providers driver to the config/auth.php file in the providers array:
'providers' => [
'users' => [
'driver' => 'custom_auth',
],
],
As im not using a database, I took out the model property
Lastly I created a folder called App/Authentication which i put my UserProvider.php file in which is this:
<?php
namespace App\Authentication;
use Illuminate\Contracts\Auth\UserProvider as IlluminateUserProvider;
class UserProvider implements IlluminateUserProvider
{
/**
* #param mixed $identifier
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveById($identifier)
{
// Get and return a user by their unique identifier
}
/**
* #param mixed $identifier
* #param string $token
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByToken($identifier, $token)
{
// Get and return a user by their unique identifier and "remember me" token
}
/**
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param string $token
* #return void
*/
public function updateRememberToken(Authenticatable $user, $token)
{
// Save the given "remember me" token for the given user
}
/**
* Retrieve a user by the given credentials.
*
* #param array $credentials
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
{
// Get and return a user by looking up the given credentials
}
/**
* Validate a user against the given credentials.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param array $credentials
* #return bool
*/
public function validateCredentials(Authenticatable $user, array $credentials)
{
// Check that given credentials belong to the given user
}
}
So lastly i make a function on the login controller. This is what the api call goes to:
public function Login(Request $request)
{
$user = Consultant::lookup('UserId', 1);
//Returns collection of user details (user id, username etc)
//Logic will go here in the future
$logThemIn = true;
if ($logThemIn)
{
auth()->login($user);
//return oauth2 token
}
}
So this is where im at now, if i run this, im getting the error:
'Declaration of App\Authentication\UserProvider::updateRememberToken(App\Authentication\Authenticatable $user, $token) must be compatible with Illuminate\Contracts\Auth\UserProvider::updateRememberToken(Illuminate\Contracts\Auth\Authenticatable $user, $token)'
Im new to laravel and there isnt alot of tutorials for what im trying to do that i can find. Any help is greatly appriciated
Change your UserProvider to this which uses Illuminate\Contracts\Auth\Authenticatable instead of App\Authentication\Authenticatable, php will load a class from the current namespace if one isn't specified.
<?php
namespace App\Authentication;
use Illuminate\Contracts\Auth\UserProvider as IlluminateUserProvider;
use Illuminate\Contracts\Auth\Authenticatable;
class UserProvider implements IlluminateUserProvider
{
/**
* #param mixed $identifier
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveById($identifier)
{
// Get and return a user by their unique identifier
}
/**
* #param mixed $identifier
* #param string $token
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByToken($identifier, $token)
{
// Get and return a user by their unique identifier and "remember me" token
}
/**
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param string $token
* #return void
*/
public function updateRememberToken(Authenticatable $user, $token)
{
// Save the given "remember me" token for the given user
}
/**
* Retrieve a user by the given credentials.
*
* #param array $credentials
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
{
// Get and return a user by looking up the given credentials
}
/**
* Validate a user against the given credentials.
*
* #param \Illuminate\Contracts\Auth\Authenticatable $user
* #param array $credentials
* #return bool
*/
public function validateCredentials(Authenticatable $user, array $credentials)
{
// Check that given credentials belong to the given user
}
}
You forgot to import Authenticatable. Just add:
use Illuminate\Contracts\Auth\Authenticatable;

Laravel deny edit posts for users if current user not author

I have model Post. How I can deny edit post, if user is not author this post?
I know that I can show user only his posts with:
Auth::user()->posts
Because model user have:
public function posts() {
return $this->hasMany('App\Models\Post');
}
But every user can go with link: http://example.com/posts/6/edit
and edit stranger post. How I can prevent this?
My controller for edit post:
/**
* Show the form for editing the specified resource.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function edit(Post $post)
{
$cats = Categories::all();
return view('sites.edit', compact('cats'));
}
I created policy:
public function edit(User $user, Post $post) {
return $user->id === $post->user_id;
}
And I can show edit form post other users.. Why?
What you need its an authorization policy
Create a policy by running : php artisan make:policy PostPolicy
Then register the creted policy in service provider :
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();
//
}
Once the policy has been registered, you may add methods for each action it authorizes. For example, let's define an update method on our PostPolicy which determines if a given User can update a given Post instance.
The update method will receive a User and a Post instance as its arguments, and should return true or false indicating whether the user is authorized to update the given Post. So, for this example, let's verify that the user's id matches the user_id on the post:
<?php
namespace App\Policies;
use App\User;
use App\Post;
class PostPolicy
{
/**
* 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;
}
}
In Your Controller :
public function edit(Post $post)
{
if(Auth::user()->id==$post->user->id){
$cats = Categories::all();
return view('sites.edit', compact('cats'));
}else{
// redirect user to home page
return redirect('/home');
}
}

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.

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