I'm working with Laravel 8.5 and I wanted to develop my own ACL.
So I made this ManyToMany relationship between Permission & User models:
User.php:
public function permissions()
{
return $this->belongsToMany(Permission::class);
}
Permission.php:
public function users()
{
return $this->belongsToMany(User::class);
}
Then I have inserted this custom permission into permissions table:
And then inserted this also into the pivot table pemrission_user:
Then at web.php, I tried this:
Route::get('/', function () {
$user = auth()->user();
// dd($user->permissions()->get());
if(Gate::allows('edit-user')){
dd(2);
}else{
dd(1);
}
});
So I tried checking if the logged in user has the permission edit-user, then shows 2 as result but now it returns 1 somehow, meaning that user has not this permission!
However if I uncomment dd($user->permissions()->get());, I can see this:
So as it shows user already has this edit-user permission but I don't why the Gate does not authorize user in this case.
So if you know, I beg you to help me cause I really don't know how to solve this...
You need to define edit-user for your Gate as well because your permission model means nothing to the Gate at the moment.
Gate::define('edit-user', function (User $user) {
return $user->permissions()->whereName('edit-user')->exists();
});
More information can be found here: https://laravel.com/docs/8.x/authorization#writing-gates
Otherwise, you can use policies:
class UserPolicy
{
public function update(User $user)
{
return $user->permissions()->whereName('edit-user')->exists();
}
}
And then to allow the user:
$user->can('update', User::make());
More information about policies can be found here: https://laravel.com/docs/master/authorization#creating-policies
There's also an open source package called laravel-permission made by Spatie that you can have a look at to learn more.
Related
I have a Location Model, which contains two properties: ID and Name.
To edit this Model, I have set up this route:
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit');
I set up very simple permissions: In the AuthServiceProvider I am checking in the boot method the following
Gate::before(function ($user, $permission) {
if ($user->permissions->pluck('name')->contains($permission)) {
return true;
}
});
Where permission is a Model that contains an ID and a name, mapped via a permission_user table.
I have these permissions set up:
edit_los_angeles
edit_new_york
edit_boston
plenty_of_other_permissions_not_related_to_location
After all this rambling, my actual question:
How can I tie these permissions to the edit the location?
The problem that I am facing is, that a given user is not allowed to edit all locations but may only be allowed to edit one location. Only the user with permission edit_los_angeles would be allowed to edit the Location with the name Los Angeles.
So I cannot group this into one permission like edit_location and add this to my route ->middleware('can:edit_location').
Instead, I would need something like this, I guess:
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit')->middleware('can:edit_los_angeles');
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit')->middleware('can:edit_new_york');
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit')->middleware('can:edit_boston');
...obviously this would not work.
What would be your approach to tackle this dilemma? :-)
Maybe I am doing something completely wrong and there is a better Laravel-Way of doing this?
Thank you very much for your help in advance!
I am using Laravel 6.0 :-)
Two assumption for my approach to work, use model binding in the controller (you should do that no matter what). Secondly there needs to be a relation between location and the permission it needs, something similar to the slug you suggested.
Your controller function would look something like this. Adding a FormRequest is a good approach for doing this logic.
class LocationController {
public function edit(EditLocationRequest $request, Location $location) { // implicit model binding
...
}
}
For ease of use, i would also make a policy.
class LocationPolicy
{
public function edit(User $user, Location $location) {
return $user->permissions->pluck('name')
->contains($location->permission_slug); // assuming we have a binding
}
}
Remember to register policy in the AuthServiceProvider.php.
protected $policies = [
Location::class => LocationPolicy::class,
];
Now in your form request consume the policy in the authorize method. From here you are in a request context, you can access user on $this->user() and you can access all models that are model binding on their name for example $this->location.
class EditLocationRequest
{
public function authorize(): bool
{
return $this->user()->can('edit', $this->location);
}
}
Now you should be able to only have a single route definition.
Route::get('administration/location/{location}/edit', 'LocationController#edit')->name('location.edit');
EDIT
Withouth the form request if you use the trait AuthorizesRequests you can do the following. This will throw an AuthorizationException of it fails.
use AuthorizesRequests;
public function edit() {
$this->authorize('edit', $location);
}
If you have a requirement based upon the location relationship, then you will need to capture that relationship in the data. A good starting point to this would be to add a pivot table specific for these editing permissions. Consider a table, location_permissions, with a user_id and a location_id. You could then modify or add permission middleware to do a check for a record in this table once you have a specific user and location.
Edit: to answer the question about implementation of middleware,
The crux of the implementation would likely be solved by defining a relationship on the user model to location via this new pivot table.
I would recommend then adding an additional method which consumes the new locations relationship to the model along the lines of
public function canEditLocation(Location $location): bool {
return $this->locations
->where('location_id', '=', $location->id)
->count() > 0;
}
And the actual middleware something along these lines:
public function handle($request, Closure $next, $location)
{
if (! $request->user()->canEditLocation($location)) {
\\handle failed permission as appropriate here.
}
return $next($request);
}
My middleware parameters knowledge is rusty, but I believe that is correct as defined at https://laravel.com/docs/master/middleware#middleware-parameters
I need to clear my doubt about Eloquent Relationships. I have 2 models User (which comes with Laravel) and Other is Role which I created.
in migration, I added role_id as an additional column as I want every user must have a role now I want to retrieve a user role based on user's id so, I created a public function named roles inside the User Model.
public function roles(){
return $this->belongsTo(Role::class);
}
now when i try to run the query like this.
App\User::find(1)->roles;
it won't return any result, just empty screen but when I change it to
public function role(){
return $this->belongsTo(Role::class);
}
after that i run code
App\User::find(1)->role;
now it returns the exact row where the user with id 1 has a proper role. it's confusing why with the roles() function it's not working but with the role() function it's working.
Sorry, If this question is already posted you can redirect me to that question.
Thank You!
You have to specify the foreign key
public function roles()
{
return $this->belongsTo(Role::class, 'role_id');
}
However, calling it role() is more accurate, since your are assuming that a User can only have one role.
I want to create a relationship that checks if a user has liked a post. In order to do this, the relationship needs to check if the user is logged in, and then use their user_id to get the like record. Something like:
public function userLike() {
if (Auth::check()) return $this->hasOne('App\Like')->where('user_id', Auth::user()->id);
}
However, this doesn't work. Additionally, if the user is not logged in and this relationship is called (which it is by default), it will return an error.
What is the proper way of doing this?
If you want to conditionally load a relation then you can do so by lazy eager loading. In your example you could define the relation as
public function userLike() {
return $this->hasOne('App\Like', 'user_id');
}
Then in your controller (or wherever else) you can do the check for if the user is question is currently logged in user or not
$loggedInUser = auth()->user();
if($loggedInUser){
$loggedInUser->load('userLike');
}
Then you can continue with whatever you want to do with the loggedInUser and the userLike.
Say you have multiple users at a point (in your code) and you want to load the likes for only the currently logged in user then you can
//$users is a collection of multiple users - assumed
foreach($users as $user){
if($user->email === auth()->user()->email){
$user->load('userLike');
}
}
Hope this helps
This is how I would do that:
1) “likes” table
I would first create a table to store all the “likes” with columns for a “post_id” and a “user_id”
2) create the relationships
User Model
public function likes()
{
return $this->belongsToMany('App\Post', 'likes', 'user_id', 'post_id');
}
Post Model
public function likes()
{
return $this->belongsToMany('App\User', 'likes');
}
3) Post Controller - the method
I would do the followings:
check if user is logged in, otherwise throw an error
make sure post exists, otherwise throw an error
create a new entry in like table with user_id and post_id
(you can also check if the user is logged in directly on your route using a middleware)
4) In the view
I would call the controller method using an ajax call.
Not sure if I forgot something, but hopefully that can help you a little bit.
If you need to “like” more things than only post, check the polymorphic relationships.
Currently my db tables are defined as below:
site_roles - Contains different roles
site_user_roles - Links Users to many roles
site_permission_modules - Contains different access levels [eg. view admin controls, ban users]
site_role_permissions - Links permission modules to roles
My models in laravel are correctly defined to query these many to many relations. In code I want to check if a user has a certain permission module linked to him.
For better understanding, here are my models as well:
User
public function SiteRoles()
{
return $this->belongsToMany('App\SiteRole', 'site_user_roles', 'users_id', 'site_roles_id');
}
SiteRole
public function Users()
{
return $this->belongsToMany('App\User', 'site_user_roles', 'site_roles_id', 'user_id');
}
public function SitePermissionModules()
{
return $this->belongsToMany('App\SitePermissionModule', 'site_role_permissions', 'site_roles_id', 'site_permission_modules_id');
}
SitePermissionModule
public function SiteRoles()
{
return $this->belongsToMany('App\SiteRole', 'site_role_permissions', 'site_permission_modules_id', 'site_roles_id');
}
Here is what I currently did:
Auth::User()->SiteRoles()->with('SitePermissionModules')->get()
The with() keyword retrieves a nested array of permissions associated with each role.
I need to check if the user has a specific permission module linked to him.
What would be the best laravel way to do this?
EDIT:
Basically I'm just asking what would be the best way to chain 2 many to many relationships.
Auth::User()->SiteRoles()->with(['SitePermissionModules' => function ($query) {
$query->where('site_role_permissions','=','admin');
//use where as per your requirment.
}])->get();
Querying Relaionship
I was simply trying to log out admin user from the Dashboard while I used this code for doing it:
Routes.php
Route::get('logout',array('uses'=>'AuthController#LogOut'));
AuthController.php
class AuthController extends Controller{
public function LogOut(){
Auth::logout();
return Redirect::to('login');
}
}
while it is giving me such error for log out
as i don't have such field in Database, and also it is not added to Database while Migration also.
The error is most likely occurring because the remember_token field is required by the Auth to be present in the users table. So you should add a remember_token field (likely a string field) in your users migration table and migrate it. Then, you should create a user, log in the user and then try logging out. Hopefully, doing this will solve your problem.
I guess you've run an update with composer recently and have upgraded the Laravel core. You'll need to perform a few steps to upgrade to the newest Laravel version as documented in the Laravel upgrade info here: http://laravel.com/docs/upgrade#upgrade-4.1.26
Laravel 4.1.26 introduces security improvements for "remember me"
cookies...change requires the addition of a new remember_token
column to your users (or equivalent) database table.
You'll also need to update your User class with these methods if you're using Eloquent:
public function getRememberToken()
{
return $this->remember_token;
}
public function setRememberToken($value)
{
$this->remember_token = $value;
}
public function getRememberTokenName()
{
return 'remember_token';
}