163
Php

Laravel – 5.1 ACL Using Middleware

It’s been quite a long time I’ve written an article. Last time in 1st January 2015, I’ve written an article about ACL using Laravel-5.0 and I had to write another follow-up article but could not do it for being busy due to my freelancing job. Still now I’m occupied and busy but probably I should finish the remaining task because I’ve got some emails and comments (as requests) in my blog for the follow-up article so I’ve finally decided to write it.

The article was about Laravel-5.0 ACL Using Middleware, before the release of Laravel 5.0 official version and this is the follow-up but slightly modified and simplified, which is for Laravel-5.1.x. In this article, I’ll demonstrate the implementation of an ACL (Access Control Layer) using Laravel Middleware from the scratch. Using this ACL system anyone can develop a permission based user access control system in their application for production.

The main diffeence is that, in the old one Laravel 5.0 didn’t allow passing parameters to middleware so I’ve used a custom key in the route’s action array to mention the required permission for the route using 'permission' => 'manage_user' and it allowed to assign a single role to a user but in this one we can pass parameters to middleware so instead of using a custom key in the action array, I’ve passed the required permission as a parameter of middleware i.e: 'middleware' => 'acl:manage_user' and also I’ve allowed to assign multiple roles to any given user but one may use it for a single role based application too. So, let’s begin.

To implement this, we need 5 tables:
  • 1. users
  • 2. roles
  • 3. permissions
  • 4. permission_role (Pivot Table)
  • 5. role_user (Pivot Table)
Database migration for users table:
increments('id')->unsigned();
            $table->string('email')->unique();
            $table->string('password');
            $table->string('first_name');
            $table->string('last_name');
            $table->rememberToken();
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('users');
    }
}
Database migration for roles table:
increments('id');
			$table->string('role_title');
			$table->string('role_slug');
		});
	}

	/**
	 * Reverse the migrations.
	 *
	 * @return void
	 */
	public function down()
	{
		Schema::drop('roles');
	}

}
Database migration for permissions table:
increments('id');
			$table->string('permission_title');
			$table->string('permission_slug');
			$table->string('permission_description')->nullable();
		});
	}

	/**
	 * Reverse the migrations.
	 *
	 * @return void
	 */
	public function down()
	{
		Schema::drop('permissions');
	}

}
Database migration for permission_role table:
increments('id');
			$table->integer('permission_id');
			$table->integer('role_id');
		});
	}

	/**
	 * Reverse the migrations.
	 *
	 * @return void
	 */
	public function down()
	{
		Schema::drop('permission_role');
	}

}
Database migration for role_user table:
increments('id');
			$table->integer('role_id');
			$table->integer('user_id');
		});
	}

	/**
	 * Reverse the migrations.
	 *
	 * @return void
	 */
	public function down()
	{
		Schema::drop('role_user');
	}

}

These tables are required to build the ACL system. The table fields could be changed (add/remove) but to build the relationship between tables we need two pivot tables and those tables should not be modified unless you know how to build many-to-many relationship between two tables using different field names than the conventional approach.

Now, we need to create the middleware class to check the user permissions and we can create it using php artisan make:middleware CheckPermission command from the command line (terminal). This will create a skeleton for a middleware class in app/Http/Middleware directory using the name CheckPermission.php. So now we need to edit that class (app/Http/Middleware/CheckPermission.php) by writing the necessary code which is given below:

guest()) {

            if ($request->user()->can($permission)) {
                
                return $next($request);
            }
        }

        return $request->ajax ? response('Unauthorized.', 401) : redirect('/login');
    }
}

Now, we need to create other classes (Eloquent Model) in app directory. So let’s create those one by one. The User.php class is available by default so you can modify that but if not present (probably deleted) then create a new one. Basically, App is used for namespacing the models and the app directory houses the classes including the default User.php but you may modify it if required, anyways, let’s create the User Model first (app/User.php):

checkPermission($permission);
    }

    /**
     * Check if the permission matches with any permission user has
     *
     * @param  String permission slug of a permission
     * @return Boolean true if permission exists, otherwise false
     */
    protected function checkPermission($perm)
    {
        $permissions = $this->getAllPernissionsFormAllRoles();
        
        $permissionArray = is_array($perm) ? $perm : [$perm];

        return count(array_intersect($permissions, $permissionArray));
    }

    /**
     * Get all permission slugs from all permissions of all roles
     *
     * @return Array of permission slugs
     */
    protected function getAllPernissionsFormAllRoles()
    {
        $permissionsArray = [];

        $permissions = $this->roles->load('permissions')->fetch('permissions')->toArray();
        
        return array_map('strtolower', array_unique(array_flatten(array_map(function ($permission) {

            return array_fetch($permission, 'permission_slug');

        }, $permissions))));
    }

    /*
    |--------------------------------------------------------------------------
    | Relationship Methods
    |--------------------------------------------------------------------------
    */
   
    /**
     * Many-To-Many Relationship Method for accessing the User->roles
     *
     * @return QueryBuilder Object
     */
    public function roles()
    {
        return $this->belongsToMany('App\Role');
    }
}

The Role class (app/Role.php):

belongsToMany('App\User');
    }

    /**
     * many-to-many relationship method.
     *
     * @return QueryBuilder
     */
    public function permissions()
    {
        return $this->belongsToMany('App\Permission');
    }
}

The Permission class (app/Permission.php):

belongsToMany('App\Role');
    }
}

Now, before we can use our Middleware in any route declaration, we need to add it it in the app/Http/Kernel.php file and by default, there are already other middlewares (aliases) added in that file by Laravel in the $routeMiddleware property (array) and we’ll add another one at the end of this variable which should look something like this after addition of 'acl' => \App\Http\Middleware\CheckPermission::class,:

/**
     * The application's route middleware.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'acl' => \App\Http\Middleware\CheckPermission::class,
    ];

The acl is an alias for our middleware (CheckPermission) and we’ll use the alias in our routes when declaring the routes for limited access, for example take a look at the following:

Route::get('/users', [
    'middleware' => 'acl:manage_user',
    'as' => 'users.all',
    'uses' => 'UserController@index'
]);

This is a route to access all users and this route is protected by the acl middleware so before the request is dispatched to the application the middleware will check if the current logged in user has the required permission which is in this case manage_user.

How it works?

Actually, this is just a demonstration of only the ACL but I didn’t provide any code which provides an user interface that let’s the admin a way to create user roles by attaching the permissions but only the core idea to implement the ACL in the application.

So, a brief idea is that, each user will be assigned one or multiple roles when user account is created from the front-end (a default role could be set from the back-end) or by the admin and the roles will be created by admin (super user) from the back-end and permissions will be attached to each roles. So, a role for example Moderator which could have a permission as Suspend User (permission title) and suspend_user (permission_slug) so we can attach suspend_user in a Route using something like this:

$router->get('user/suspend/{id}', [
    uses' => 'UserController@closeUserAccount',
    'as' => 'admin.user.suspend',
    'middleware' => ['acl:suspend_user']
]);

In this case, the route declared above requires the permission suspend_user and in the acl/CheckPermission middleware we can check the user’s permission and can protect the route if the user doesn’t has suspend_user permission. That’s it.

Note:

Notice that, this middleware also checks if the user is logged in or not so no need to use auth middleware with this and without being log in this won’t allow the use to visit any route. Also, I’ve used the Guard/Auth and didn’t use dependency injection, instead I’ve fetched the object from the service container (IoC) and it’s not a problem but you may use a constructor injection by typehinting in the __construct method of the middleware and check the old/previous article for an example of that.

Also, if you need to access any route paraneter inside the handle method then you may use somethinbg like this:

    // Get the id from URL (http://example.com/user/1)
    $id = $request-route()->parameter('id'); // 1

Also it’s worth remembering that, Route::parameters() method returns an array of all route parameters. For RESTful Resource controllers you may use __construct method to hook up the middleware using something like the following:

public function __construct()
{
    $this->middleware('acl'); // To all methods

    $this->middleware('acl', ['only' => ['create', 'update']]); // Only specified methods

    $this->middleware('acl', ['except' => ['index', 'show']]); // All but specified ones
}

In this article, I tried discussed how to organize the tables and classes and how to filter the requests using middleware and what classes could be required but not full implementation of a fully functional system, it just gives an abstract idea to create an ACL functionality using Laravel - 5.1.x Middleware from the scratch (without using any third party package).

P/S: Some text are copied from old article so pardon me for any kind of silly mistakes. Also apologies for taking the time for writing this article even I was commited to write it months ago. Hope you would like it and your feedback is always appriciated. Thanks!

Latest Blog

0
Php

PHP – 8.0 Match Expression

In PHP 8.0 there is a new feature or I should say a new language construct (keyword) going to be introduced,  which has been implemented depending […]