I am creating an application using Laravel 5.1 with users, roles and actions.
The table setup is like so:
user
id name
1 John Smith
2 Fred Smith
role
id name
1 Administrator
2 Customer
role_user
user_id role_id
1 1
2 1
action
id name description path
1 dashboard ability to access dashboard /admin
2 editAdmin ability to edit admins /admin/users/administrators/{id}/edit
action_role
action_id role_id
1 1
2 1
The user table holds ALL users on the site, including administrators and customers.
The role table holds all the possible roles a user can have. For example Administrator or Customer.
The role_user table is a pivot table which links role to user.
The action table lists all of the actions possible (i.e. urls or routes) on the app.
The action_role is a pivot table which links action to role.
So to summarise:
Users have roles
Roles have actions
A user can have many roles
A role can have many actions
I want to have a middleware setup which checks on page load if the user has permissions to view the current page. In order to do this, I need to be able to access a users actions, using a call like this:
$user->roles()->actions();
How do I setup my eloquent relationships to support this kind of call?
UPDATE
My relationships are setup like so in my models:
User
/**
* The roles that belong to the user.
*
* #return Object
*/
public function roles()
{
return $this->belongsToMany('App\Models\User\Role')->withTimestamps();
}
Role
/**
* The actions that belong to the role.
*
* #return Object
*/
public function actions()
{
return $this->belongsToMany('App\Models\User\Action');
}
/**
* The users that belong to the role.
*
* #return Object
*/
public function users()
{
return $this->belongsToMany('App\Models\User\User');
}
Action
/**
* The roles that belong to the action.
*
* #return Object
*/
public function roles()
{
return $this->belongsToMany('App\Models\User\Role');
}
Eloquent does not have a HasManyThrough relationship across 2 pivot tables.
1. You can get the actions by lazy eager loading the roles and actions, then extracting them:
$actions = $user->load('roles.actions')->roles->pluck('actions')->collapse()->unique();
2. You can check the action directly in the database, using a whereHas constraint:
$allowed = $user->roles()->whereHas('actions', function ($query) use ($action) {
$query->where('name', $action);
})->exists();
For handling user, user roles and user permissions you can simply use Toddish package.
There are lot of things this package does for you. like:
$user->is('Your Defined Role');
$user->can('add_user');
$user->level(7);
For installation just read it's documentation.
Although you have mentioned that your Eloquent models (and their relationships) are set up, I am assuming that you have the following in your application already:
User model
class User extends Eloquent {
public function roles() {
return $this->belongsToMany('App\Models\Role');
}
}
Role model
class Role extends Eloquent {
public function actions() {
return $this->belongsToMany('App\Models\Action');
}
public function users() {
return $this->belongsToMany('App\Models\User');
}
}
Action model
class Action extends Eloquent{
public function roles(){
return $this->belongsToMany('App\Models\Role');
}
}
Given the above set properly, you can't certainly make a call like the following as Laravel will assume you're making a call to a Query Builder method or whatever [which in fact doesn't exist]:
$userActions = App\Models\User::find(1)->roles()->actions();
Instead, you have to use Laravel's magical whereHas method to query relations where multiple nested relations are involved.
Now, having the user's id and the current allowed action [which indeed can be utilized in your middleware], it can be determined whether the user is allowed to see the page:
$hasAccess = App\Models\User::whereHas('roles.actions', function($q) use ($id, $action){
$q->where('users.id', $id);
$q->where('actions.name', $action);
})->get()->toArray();
if($hasAccess){ // or `count($hasAccess) > 0`. The former will work since any number > 0 evaluates to true in PHP
//user has access
}
Notice the nesting of relationships with ..
Relationship Existence in Laravel:
In Laravel the existence of a relationship between models is determined with has and whereHas methods. The has method is used to only determine if a relationship exists, e.g. by executing App\User::has('roles')->get() you'll always get a list/collection of users which at least have any roles.
More power to this can be added with whereHas with which you can add where clauses to the actual query.
Hi this is how I solved this.
I didn't use an actions table. Just users, roles and the user_role table.
After the solution, I pass my custom middleware a role array to every route I create like:
Route::get('insights',[
'as'=>'insights',
**'middleware'=>['access.level'],**
**'roles'=>['admin','customer'],**
'uses'=>'CustomersController#index'
]);
Files modified:
app\Http\Controllers\Controller.php
Custom middleware: CheckAccessLevel
app\Http\Kernel.php
app\Http\routes.php
Controller.php
In this file, I wanted to eager load the current logged in user with his roles and make it a global variable in all views as {{currentUser}} and in all my controllers as "$this->currentUser". this is the code:
protected $currentUser;
public function __construct() {
if(isset(Auth::user()->username)){
$this->currentUser = User::with('roles')->where(['username' => Auth::user()->username])->first();
// Share this property with all the views and controllers in your application.
view()->share('currentUser', $this->currentUser);
}
}
Custom Middleware: CheckAccessLevel.php
Over here in the middleware, I retrieve the roles array passed to the route and and also the roles assigned to the current user.
After i get these variables, I intersect them to see if there is a match before I pass the user on. check the code below:
//if user is not logged in, show them where to login :)
if (!Auth::check()) {
return redirect()->route('user::login');
}
//Otherwise get the roles for that route
$get_route_action = $request->route()->getAction();
$get_route_roles = $get_route_action['roles'];
//Eager load the user with their list of roles
$eager_user = User::with('roles')->where(['id'=>$request->user()->id])->first();
$user_roles = $eager_user->roles->pluck('role_name')->toArray();
//intersect the users roles with the route roles to see if there is a match
foreach ($user_roles as $user_role){
if(in_array($user_role, $get_route_roles)){
return $next($request);
}
}
Kernel.php
over here, i register my route as "access.level" inside the route middleware group
Route.php
then whenever I create a route, I just pass in the allowed role names to it and voala! It works for me..
I hope this helps.
You need a HasManyThrough relationship here.. Here's something to get you started.
Judging by your given tables I see it has a one-to-many relationship with user and roles. While action and roles also has many-to-many relationship.
But first you need to create models for each entities(user,role,action) to follow the MVC structure. You could easily make these models from the command line using the Laravel's artisan with this command.
php artisan make:model User
Make sure to also add or change columns in the newly created migrations. Assuming you also have your pivot tables set up for the 3 tables, you can now add the relationships.
class User extends Model {
//
protected $table = 'users';
/*
* #var table
*
*/
public function action()
{
return $this->hasManyThrough('App\Action', 'App\Role');
}
}
Now you have a HasManyThrough relationship. you can query like this directly no need to include role.
//this
$actions = User::find(1)->action();
//instead of
$actions = User::find(1)->role()->action();
//this will return all actions available to a user with id of 1
NOTE I also suggest you make your role and user models to be on a one to one relationship.
Related
I am designing a CMS and i have setup users based on the role.How do i limit the users of their permissions based on their access level?
The easiest way is to get users by their role. Have a column for your users table called role or whatever you name it.
You can do Access Level Control easily with Gates
In your app\Providers\AuthServiceProvider register your policy. Example:
use Illuminate\Support\Facades\Gate;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
public function boot(GateContract $gate)
{
$this->registerPolicies($gate);
$gate->define('isUser', function($user){
return $user->role == 'user';
});
$gate->define('isDealer', function($user){
return $user->role == 'dealer';
});
}
isUser , isDealer are the user Types we are defining to Use in the project blade,controllers.You can change it as you like.Role is the column that you created in the table and we are comparing with the table values which are the user types user and dealer.
you can limit values in blade with laravel method
#can('isUser')
<only visible to users based on role user>
#endcan
It will be still accessible via routes so you can limit via controller functions or routes.
//controller
public function create()
{
if(!Gate::allows('isUser')){ // || for multiple parameters can('isAdmin' || 'isUser)
abort(404,"Abort");
}
return view('yourView');
}
This way the controller function will be not accessible for the roles defined.
Check the official documentation for in detail methods and information.
I'm new with Laravel or any MVC.
I created the models, I create the route from web.php, I created the view with Blade, I created the controller and fetched all the data from a single model. Now, I want to make a join or something similar.
I want to get something this:
Name | Section name | Status.
In my user Model I have:
ID | Name | SectionID | Status
In my sections Model I have:
ID | SectionID | Active
Currently I created the controller like this:
public function show($name)
{
if(User::where('name', '=', $name)->exists())
{
return view('profile', ['user'=>User::where('name', $name)->first()]);
}
else
{
return 'User not found';
}
}
And in my Blade I'm getting content like this: {{ $user->name }}
Hmm you have to define the relationship between user and section model.
Please check this thread for how to work with relationships in laravel Laravel Eloquent Relationships.
Then whenever you get the user you can load any relationship that its defined on User model.
In your case you are performing two unecessary database calls, so I would suggest you to check out DRY principle.
Lets say in User has one Section you define the relationship in User model like so:
public function sections(){
return $this->hasOne(Section::class);
}
Then the reverse relationship from section model:
public function user(){
return $this->belongsTo(User::class);
}
So now your method in controller might look something along the lines of this:
public function show($name){
return view('profile', ['user'=>User::with('sections')->where('name', $name)->firstOrFail()]);
}
This way you eagerload the sections that are tied to that user, for more about Laravel eagerloading
Then in the blade you can access any of the attributes from user or section model.
public function __construct()
{
$this->middleware('roles:Author')->only(['index','show','create']);
$this->middleware('roles:User')->only(['index','show']);
}
In my controller i want access methods as per users role for e.g if user role is admin then he has to access all methods of controller, if user role is Author then he has access of index,create and show method and if role is User then he has only access of index and show method.
You can take a look at Gates (docs).
In your App\Providers\AuthServiceProvider add:
Gate::define('create-post', function ($user) {
return $user->isAuthor(); //Here you should check the users role
});
And then in your controller's method create():
if (Gate::allows('create-post')) {
// The current user can create posts...
}
The other two methods: index() and show() are available to both roles so there's no action required.
I have this path:
http://localhost:8000/home
I want when a regular user opens path above, then I call this controller:
mainPage#index
But when a admin opens that path, then I call this controller:
panelPage#index
So as you see, I'm looking for "dynamic routes" kinda .. Is implementing that possible? In other word, can I call two different controllers for admin and regular member ?
This is a good case to use Middlewares to filter HTTP requests.
You could also do something conditional in your routes file, like:
if (Auth::user()->isAdmin()){
Route::get('/', 'panelPage#index');
}
else {
Route::get('/', 'mainPage#index');
}
Depending on what your application looks like, you can define isAdmin() in your User model. This is a very simple example where you have a column called role_id and id nr 1 equals admin. If authenticated user is admin, it displays true, otherwise false:
public function isAdmin()
{
return Auth::user()->role_id == 1;
}
A more dynamic and advanced approach would be to create a role table, and associate the role with the user with a role_user pivot table.
If you want it to take a step further, you can create a permissions table and associate the role with the permissions with a permission_role pivot table. Then you can in your application define that a permission is needed to be able to do an action and add all the permissions that a given user role has in that pivot table. Then you just check if the user (with a specific role) has the given permissions.
For best practice, you could use Middleware to sort-out and categorize your routes and controllers.
In this - relatively simple - case, you could also use something like this (in your routes file):
if(!is_null(Auth::user())) {
// first check if user is logged in, else Auth::user() will return null
$uses = 'mainPage#index';
if(Auth::user()->admin) {
$uses = 'panelPage#index';
}
Route::get('/', $uses);
}
Update
Or you could wrap everything inside this if statement in an auth middleware group, like this:
Route::group(['middleware' => ['auth']], function(){
$uses = 'mainPage#index';
if(Auth::user()->admin) {
$uses = 'panelPage#index';
}
Route::get('/', $uses);
});
Also make sure that your users table has a column named 'admin'.
On my application I use the default Laravel ACL to define abilities for the users, associated with a Role and Permissions subsequently.
I have a hasMany and a belongsTo relationship set up, in which a User belongs to a Company model and a Company has many Users. I would like to define "types" of companies which have different abilities, separately from the User abilities. For example, a company might be an "Architect" company with different abilities than a "Contractor" company, while each company has a User with a role of "Company Administrator", which can add or delete users from their company, and a bunch of "Regular" users.
Right now I have working the part in which a user can have a role, but I am a little bit lost on how to implement the Company "type or role". I am thinking that I must create my own AuthServiceProvider, name it something else and register it within laravel service providers, along with my own implementation of Gate that injects the Company model instead of the User?
Right now I am defining my User abilities in my AuthServiceProvider, and checking using the Gate Facade, for example:
Register Abilities in AuthServiceProvider.
//AuthServiceProvider
/**
* Register any application authentication / authorization services.
*
* #param \Illuminate\Contracts\Auth\Access\Gate $gate
* #return void
*/
public function boot(GateContract $gate)
{
parent::registerPolicies($gate);
foreach ($this->getPermissions() as $permission) {
$gate->define($permission->name, function ($user) use ($permission) {
return $user->hasPermission($permission);
});
}
}
Then check User abilities on UserController.
//UserController
/**
* Edit the user's email.
*
* #param User $user
*/
public function edit(User $user)
{
if(Gate::allows('edit', $user){
$user->email = $this->request->input('email');
$user->save();
}
}
I would like to be able of doing the same kind of checks with the Company model, i.e.:
// Check if the Company that the user belongs to is allowed to create posts
CompanyGate::allows('create-post');
Currently on your User model you seem to have defined a hasPermission function.
You can simply create a similar method on your Company model which checks the roles and permissions of a given company.
If you want to use Gate you still need to check permissions via the authenticated user, it is always going to validate permissions in the context of the authenticated user - but as the user belongs to a company you can then hop along to the company's permissions.
Something similar to the following:
$gate->define('create-post', function ($user) {
return $user->company->hasPermission('create-post');
});