I am working on a blogging application in Laravel 8.
The application gives the users rights by assigning them roles. Every role has a set of permissions. There is a many-to-many relationship between roles and permissions.
In the user-rights view, I output each user's permissions successfully:
#foreach ($user->role->permissions as $permission)
<span class="badge bg-primary">{{ $permission->slug }}</span>
#endforeach
The goal
I am trying to restrict access to the Site settings section of the application, like this:
// Settings routes
Route::group(['prefix' => 'settings', 'middleware' => ['checkUserPermissions:edit-settings']], function() {
Route::get('/', [SettingsController::class, 'index'])->name('dashboard.settings');
});
For this purpose, I have created the checkUserPermissions middleware:
class CheckUserPermissions
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* #return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
// Permissions checker
public function hasPermissionTo($permission) {
return in_array($permission, Auth::user()->role->permissions->toArray());
}
public function handle(Request $request, Closure $next, ...$permissions)
{
// Check user permissions
foreach ($permissions as $permission) {
if (!$this->hasPermissionTo($permission)) {
$permission_label = join(' ', explode('-', $permission));
return redirect()->back()->with('error', 'You do not have permission to ' . $permission_label);
}
}
return $next($request);
}
}
The problem
Although the super admin does have the permission to edit settings, dd(in_array($permission, Auth::user()->role->permissions->toArray())) returns false.
That means that the restriction(s) apply when they should not.
NOTE
dd(Auth::user()->role->permissions->toArray()) returns:
Questions
What causes this bug?
What is the easiest fix?
In your custom middleware, you need to compare the permission to the slugs
return in_array($permission, Auth::user()->role->permissions()->pluck('slug')->toArray());
Since you're eager loading permissions, Eqloquent will return a collection of models. Inside of the Collection instance, there is a pluck method that will create a new collection of columns.
Using the toArray method, you will then end up with the expected array that your $permission will match to:
Auth::user()->role->permissions->pluck('slug')->toArray()
An alternative way would be to use PHP native methods (array_column) to achieve this. You could then return a list back to the view of all of the missing permissions as apposed to a single missing permission. This is untested but should just work out the box:
public function handle(Request $request, Closure $next, ...$permissionsToTest)
{
$permissions = array_column(Auth::user()->role->permissions->toArray(), 'slug');
$results = array_map(fn(string $permission): bool => in_array($permission, $permissions), $permissionsToTest);
$failedResults = array_keys(array_filter($results, fn(bool $result): bool => !$result));
$missingPermissions = array_map(fn(int $key): string => 'You do not have permission to ' . implode(' ', explode('-', $permissionsToTest[$key])), $failedResults);
# Utilise the built-in "errors"
return empty($missingPermissions) ? $next($request) : redirect()->back()->with('errors', $missingPermissions);
}
For a vanilla PHP mock-up example, See it 100% working over at 3v4l.org
Related
I actually am not able to understand why I am getting the following error.
App\Models\User::team must return a relationship instance, but "null" was returned. Was the "return" keyword used?
I am basically creating test cases for simple orders for ecommerce.
User Modal
public function team(): BelongsTo|null
{
if (!empty($this->team_id)) {
return $this->belongsTo(Team::class);
}
return null;
}
Test case
public function test_order_status_update()
{
$order = $this->create_order($this->books->id, $this->appUser->id, $this->address->id);
$response = $this->actingAs($this->user)->put('orders/' . $order->json('order.id'), [
'order_status' => 'ordered',
]);
$response->assertRedirect('orders/' . $order->json('order.id'))->assertSessionHas('success');
}
In addition, I have another feature in my application called pages access control, which controls page access for multiple users (admin, developer, and users).
I have implemented this feature manually using middleware.
Middlware.php
public function handle(Request $request, Closure $next)
{
//teams 1-Developer 2-Admin 3-Management 4-Marketing 5-Audit 6-Sales 7-Bookstores 8-Delivery 9-User
$team = $request->user()->team;
if ($team->id == 1 || $team->id == 2) {
return $next($request);
}
$pages = auth()->user()->pages->merge(auth()->user()->team->pages);
$currentRouteName = $request->route()->getName();
$pages->contains('route_name', $currentRouteName) ?: abort(403);
return $next($request);
}
Based on the error above, I believe the actingAs function is unable to obtain authenticated user information, which is why my test failed.
How can I fix this?
Simply don't check your team_id:
public function team(): BelongsTo
{
return $this->belongsTo(Team::class);
}
Laravel tries to be smart. If team_id isn't set, it will just return null. However, if you don't return the BelongsTo, the magic code of Laravel will trip when you try to access user->team
I have a notes model. Which has a polymorphic 'noteable' method that ideally anything can use. Probably up to 5 different models such as Customers, Staff, Users etc can use.
I'm looking for the best possible solution for creating the note against these, as dynamically as possible.
At the moment, i'm adding on a query string in the routes. I.e. when viewing a customer there's an "Add Note" button like so:
route('note.create', ['customer_id' => $customer->id])
In my form then i'm checking for any query string's and adding them to the post request (in VueJS) which works.
Then in my controller i'm checking for each possible query string i.e.:
if($request->has('individual_id'))
{
$individual = Individual::findOrFail($request->individual_id_id);
// store against individual
// return note
}elseif($request->has('customer_id'))
{
$customer = Customer::findOrFail($request->customer_id);
// store against the customer
// return note
}
I'm pretty sure this is not the best way to do this. But, i cannot think of another way at the moment.
I'm sure someone else has come across this in the past too!
Thank you
In order to optimize your code, dont add too many if else in your code, say for example if you have tons of polymorphic relationship then will you add tons of if else ? will you ?,it will rapidly increase your code base.
Try instead the follwing tip.
when making a call to backend do a maping e.g
$identifier_map = [1,2,3,4];
// 1 for Customer
// 2 for Staff
// 3 for Users
// 4 for Individual
and so on
then make call to note controller with noteable_id and noteable_identifier
route('note.create', ['noteable_id' => $id, 'noteable_identifier' => $identifier_map[0]])
then on backend in your controller you can do something like
if($request->has('noteable_id') && $request->has('noteable_identifier'))
{
$noteables = [ 'Customers', 'Staff', 'Users','Individual']; // mapper for models,add more models.
$noteable_model = app('App\\'.$noteables[$request->noteable_identifier]);
$noteable_model::findOrFail($request->noteable_id);
}
so with these lines of code your can handle tons of polymorphic relationship.
Not sure about the best way but I have a similar scenario to yours and this is the code that I use.
my form actions looks like this
action="{{ route('notes.store', ['model' => 'Customer', 'id' => $customer->id]) }}"
action="{{ route('notes.store', ['model' => 'User', 'id' => $user->id]) }}"
etc..
And my controller looks this
public function store(Request $request)
{
// Build up the model string
$model = '\App\Models\\'.$request->model;
// Get the requester id
$id = $request->id;
if ($id) {
// get the parent
$parent = $model::find($id);
// validate the data and create the note
$parent->notes()->create($this->validatedData());
// redirect back to the requester
return Redirect::back()->withErrors(['msg', 'message']);
} else {
// validate the data and create the note without parent association
Note::create($this->validatedData());
// Redirect to index view
return redirect()->route('notes.index');
}
}
protected function validatedData()
{
// validate form fields
return request()->validate([
'name' => 'required|string',
'body' => 'required|min:3',
]);
}
The scenario as I understand is:
-You submit noteable_id from the create-form
-You want to remove if statements on the store function.
You could do that by sending another key in the request FROM the create_form "noteable_type". So, your store route will be
route('note.store',['noteableClass'=>'App\User','id'=>$user->id])
And on the Notes Controller:
public function store(Request $request)
{
return Note::storeData($request->noteable_type,$request->id);
}
Your Note model will look like this:
class Note extends Model
{
public function noteable()
{
return $this->morphTo();
}
public static function storeData($noteableClass,$id){
$noteableObject = $noteableClass::find($id);
$noteableObject->notes()->create([
'note' => 'test note'
]);
return $noteableObject->notes;
}
}
This works for get method on store. For post, form submission will work.
/**
* Store a newly created resource in storage.
*
* #param \Illuminate\Http\Requests\NoteStoreRequest $request
* #return \Illuminate\Http\Response
*/
public function store(NoteStoreRequest $request) {
// REF: NoteStoreRequest does the validation
// TODO: Customize this suffix on your own
$suffix = '_id';
/**
* Resolve model class name.
*
* #param string $name
* #return string
*/
function modelNameResolver(string $name) {
// TODO: Customize this function on your own
return 'App\\Models\\'.Str::ucfirst($name);
}
foreach ($request->all() as $key => $value) {
if (Str::endsWith($key, $suffix)) {
$class = modelNameResolver(Str::beforeLast($key, $suffix));
$noteable = $class::findOrFail($value);
return $noteable->notes()->create($request->validated());
}
}
// TODO: Customize this exception response
throw new InternalServerException;
}
I am currently using a route with a parameter nameto query the profile of a user in my application.
So for example: /members/johnwill show the profile of John.
You see the problem here, if there are 2 John's then it's gonna be a problem.
I know I could do something like this /members/idsince id is unique but I want the url to look pretty with the user's name and not a random number.
So my question is if there is a way to use the id to make it unique but to display the name in url?
my route:
Route::get('/members/{name}', 'UserController#usersProfile');
My usersProfile method:
/**
* Returns a users profile.
*
* #param string $name
*
* #return \Illuminate\Http\Response
*/
public function usersProfile($name)
{
$profile = $this->userService->getProfile($name);
if ($profile == null) {
return redirect('members')->with('status', 'Whoops, looks like that member does not exist (yet).');
}
return view('members/profile', ['profile' => $profile]);
}
you can use some package for slug your model name :
https://github.com/spatie/laravel-sluggable
https://github.com/cviebrock/eloquent-sluggable
these packages automatically doing that for you.
if you have same name this will happen:
http://example.com/post/my-dinner-with-andre-francois
http://example.com/post/my-dinner-with-andre-francois-1
http://example.com/post/my-dinner-with-andre-francois-2
just in production i have some issue with scopes...
when you apply global scope on model , it may create duplicate slug for that ... you can fix that by add this to your model :
public function scopeFindSimilarSlugs(Builder $query, Model $model, $attribute, $config, $slug)
{
$separator = $config['separator'];
return $query->withoutGlobalScopes()->where(function (Builder $q) use ($attribute, $slug, $separator) {
$q->where($attribute, '=', $slug)
->orWhere($attribute, 'LIKE', $slug . $separator . '%');
});
}
You should add another column to your user table with name 'slug' or 'username' and map that column in your route.
In your users model, make a static function to generate unique slug with a number attached and save it in that new column to provide users with public/private profile URLs.
This below sample will stop returning slug/username after 100 users with the same name.
public static function getUniqueSlug($name) {
$original_slug = getSlug($name); // Default getter to get value in model
$slug = str_slug($original_slug); //Convert a string into url friendly string with hyphens
while(true) {
$count = User::where('slug', $slug)->count();
if($count > 0) {
$slug = $original_slug."-".rand(1,99);
} else {
return $slug;
}
}
}
based on documentation https://laravel.com/docs/5.5/authorization#via-middleware
use App\Post;
Route::put('/post/{post}', function (Post $post) {
// The current user may update the post...
})->middleware('can:update,post');
the 'post' in 'can:update,post' is variable passed from '{post}'.
im trying to use middleware('can:update,1'). its not working. maybe its search for '$1' variable, how to pass number '1' to 'can' middleware?
update this is the gate:
Gate::define('update', function ($user, $role){
$id = $user->id;
$roles = $user::find($id)->roles()->get();
$roleid = $roles[0]->pivot->role_id;
//above code just for get role id, and $role_id is 1
return $roleid === $role;
});
Probably you don't have Policy created for Post.
You can create by command:
php artisan make:policy PostPolicy --model=Post
and then implement method update in that policy.
I to had this same problem so I did some digging into the 'can' middleware (Which maps to Illuminate\Auth\Middleware\Authorize)
Once in the class we see the following code
/**
* Get the model to authorize.
*
* #param \Illuminate\Http\Request $request
* #param string $model
* #return \Illuminate\Database\Eloquent\Model|string
*/
protected function getModel($request, $model)
{
if ($this->isClassName($model)) {
return trim($model);
} else {
return $request->route($model, null) ?:
((preg_match("/^['\"](.*)['\"]$/", trim($model), $matches)) ? $matches[1] : null);
}
}
What this means is...
If our string passed in is a class name then return that class name
If it is not a class name then...
1 Try to get it from the route, then return the route param
2 Try to get the model from the string via the regex "/^['\"](.*)['\"]$/"
So now lets say we have the middleware call of
$this->middleware(sprintf("can:create,%s,%s", User::class, Role::SUPPORT));
This will not work because the Role::SUPPORT does not match the regex
To match it we simply need to place the Role::SUPPORT into quotes.
TAKE NOTE OF THE "'" around the second %s
$this->middleware(sprintf("can:create,%s,'%s'", User::class, Role::SUPPORT));
To answer your question specifically, quote your '1' value
middleware("can:update,'1'").
I am building an application that uses the repository pattern. Now what I have done is reuse functionality but I have hit a bit of a hiccup. I have one view that shows several different models on it. These models are related through a one-to-one relationship (MovieBasic to MovieDetail) and one-to-many relationship (MovieBasic to MoviePersonnel). The issue I am having is that I have two different request to validate my forms. They are MovieBasicRequest, which validates my movie's basic information (Title, synopsis) and MovieDetailRequest, which validates my movie's detail information (price, screen type, runtime, etc). So to distinguish between which request to use I have added a parameter to my url as follows:
movie_basic.blade.php
<?php $params = ['id' => $movie->id, 'type' => 'movie_basic']; ?>
<h4>Movie Baiscs <span class="pull-right">Edit</span></h4>
<hr>
<table class="table table-bordered">
<tbody>
<tr>
<td>{{ $movie->movie_title}}</td>
</tr>
<tr>
<td>{{ $movie->movie_synopsis }}</td>
</tr>
</tbody>
</table>
I know that using the <?php ?> tags is not best practice but I will clean that up later. So because of my $params the URL will look like so
www.moviesite.dev/1/edit?movie_basic
Which will call the edit function in the controller like so
MovieController.php
/**
* Show the form for editing the specified resource.
*
* #param int $id
* #return Response
*/
public function edit($id)
{
$movie = $this->movieBasic->find($id);
return view('cms.edit', compact('movie', 'type'));
}
In this case the type does not really play a role because of the relationship between MovieBasic and MovieDetail models. However it does play a role in my update function below:
MovieController.php
/**
* Update the specified resource in storage.
*
* #param int $id, MovieBasicRequest $request
* #return Response
*/
public function update($id)
{
if(strcmp($_GET['type'], 'movie_basic') == 0)
{
$movie = $this->movieBasic->find($id);
$this->request = new MovieBasicRequest;
$this->movieBasic->update($id, $this->request);
}
elseif(strcmp($_GET['type'], 'movie_detail') == 0)
{
$movie = $this->movieBasic->find($id);
$this->request = new MovieDetailRequest;
$this->movieDetail->update($id, $this->request);
}
return redirect()->action('MovieController#show', compact('movie'));
}
Essentially what this function does is determine what is being passed in and from there call the correct request. However the way I have it now it just creates an empty array and thus validates nothing. Is there any way to uses these requests to validate information passed in? Or to validate input before I pass it to the update function of the repository?
PS. I have also tried this:
$this->movieBasic->update($id, MovieBasicRequest $request);
but I get an "Undefined variable $request" error.
You should better combine them. And you can use sometimes on your form validation for handling both where you will only validate present fields. So that your MovieRequest can be like below
class MovieRequest extends Request
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'movie_title' => 'sometimes|required|min:3',
'price' => 'sometimes|required|integer'
// and so on
];
}
}
So you can update your controller as below and use for the both pages. For instance, if the price is not set within the request, then it will skip validating it, but if it's present and empty, then it will throw an error message as it's required.
public function update($id, MovieRequest $request)
{
$movie = $this->Movie->find($id);
$movie->fill($request->all());
$movie->save();
return redirect()->action('MovieController#show', compact('movie'));
}