Guardian - Middleware based permission control for Laravel

I'm working on a package for permission control using middleware in Laravel. I have 3 overall goals:

  • Exclusively defined by middleware
  • Global and Local access
  • Support for Saas

I have already finished most of the work, but it needs more testing before I consider it done. Actually no tests have been written, only manual tests. Also I'm still in the stage of considering if changes should be made and this is where I hope to get some community feedback.

Middleware

The ideas of using middleware is several. First of all it allows me to separate the permission logic out of my classes and into either annotations or routes.php. Second it gives a quick and nice overview what permissions are required where if you run php artisan route:list.

The middleware parameters are worked to be as simple as possible. All you have to write is for instance

'middleware' => 'guard:project'

The package will then behind the scenes check if you have global or local access (more about this in the next section) to the project. You don't have to mess around with IDs or anything. If you have access, of course the flow is as usual, if not you can either define in the config file to return to a URL or by default an abort(403)will be sent back.

It is also possible to guard with several permissions simply by separating by :

'middleware' => 'guard:project:group'

Global and Local access

The package do require a set of 5 tables to migrated in order to add all the functionality. The tables are:

  • permissions
  • roles
  • roles_permissions
  • users_roles
  • users_permissions

All table names can be customised in the config file upon installation of the package. The 2 tables with users in them take the name of your User class' table name, so if your users table is named user then it would be user_access and so on. The same apply to relevant column names.

Permissions are the link to the guard middleware. Not so much to tell about this, other than the permission.name is what you use as guard parameters.

Roles are used to group a set of permissions.

Roles_permissions link those two together.

Users_roles link a role to an user id which then have all the grouped permissions. It is possible (only in database) to set a locked parameter, which will prevent any changes to the relation. The role can still be changed, I plan for allow locking roles too. The idea is that there might be 1 owner of the company or similar, who of course does not want an employee to remove his access.

Users_permissions is where the local access come in. There are 3 key columns in this table:

  • foreign_id
  • user_id
  • permission_id The purpose of this table is to grant a user access to for example a project with id 5. You would then have this permissions table
|  id  |  name     |
--------------------
|  1   |  project  |

In users_permissions you would add this to grant access to project 5 to user 2

|  foreign_id  |  user_id  |  permission_id  |
----------------------------------------------
|  5           |  2        |  1              |

You still don't need to add extra stuff to your middleware as it will automatically first check for global access and then for local access.

Support for SaaS

This is where I'm working at the moment of writing. The idea is to let you have several customers in your application separated by something that is configurable to Guardian. Currently this is how the config looks according to having multiple customers:

   /*
    |--------------------------------------------------------------------------
    | Multi Customer Installation
    |--------------------------------------------------------------------------
    |
    | This setting should be set before you migrate and not changed after.
    | Also be careful to change your architecture related to your user
    | and customer specific logic.
    |
    | If your application serves several customers within the same application
    | this is where you want to assign which model that serves your customers.
    | Are upi hosting a Software as a Service (SaaS) solution, this is where
    | you want to reference, how your customers are identified.
    |
    | A lookup on Auth::user() will be performed so you want to return a
    | hasOne(Customer::class) or similar from your users model.
    |
    | Available settings are: array or null.
    |
    */

    'customer' => ['customer', 'id'],

I'm open for ideas.

My idea is that permissions are always set global since they need to be hardcoded into your source. For this reason they are no indenpendent to customers. However I want roles to be independent to customers so that you can let your customers decide which permissions a given role is required and also let them custom name it. I know from my own clients they have some very uncommon names for this which I can't apply to other clients.

I might rename customer to client, as I feel it makes more sense.

The tricky part is to determine local access with a client based system. I don't want any client accidentally be able to allow their users to access another clients data of course. So this I'm still working on. The hard part is how I check foreign_id without knowing every database structure in the universe.

API

I do have added a bit of API. The API is mainly made to serve the future Vue implementation, but is also a nice handy feature for anyone who wants to make their own GUI without dig deep into my code.

The API from route:list

|        | GET|HEAD  | api/guardian/access                                         | api.guardian.access.index             | EmilMoe\Guardian\Guardian\API\AccessController@index             | auth              |
|        | POST      | api/guardian/access                                         | api.guardian.access.store             | EmilMoe\Guardian\Guardian\API\AccessController@store             | auth              |
|        | GET|HEAD  | api/guardian/access/{access}                                | api.guardian.access.show              | EmilMoe\Guardian\Guardian\API\AccessController@show              | auth              |
|        | DELETE    | api/guardian/access/{access}                                | api.guardian.access.destroy           | EmilMoe\Guardian\Guardian\API\AccessController@destroy           | auth              |
|        | GET|HEAD  | api/guardian/permissions                                    | api.guardian.permissions.index        | EmilMoe\Guardian\Guardian\API\PermissionsController@index        | auth              |
|        | POST      | api/guardian/permissions                                    | api.guardian.permissions.store        | EmilMoe\Guardian\Guardian\API\PermissionsController@store        | auth              |
|        | DELETE    | api/guardian/permissions/{permissions}                      | api.guardian.permissions.destroy      | EmilMoe\Guardian\Guardian\API\PermissionsController@destroy      | auth              |
|        | PUT|PATCH | api/guardian/permissions/{permissions}                      | api.guardian.permissions.update       | EmilMoe\Guardian\Guardian\API\PermissionsController@update       | auth              |
|        | GET|HEAD  | api/guardian/permissions/{permissions}                      | api.guardian.permissions.show         | EmilMoe\Guardian\Guardian\API\PermissionsController@show         | auth              |
|        | GET|HEAD  | api/guardian/roles                                          | api.guardian.roles.index              | EmilMoe\Guardian\Guardian\API\RolesController@index              | auth              |
|        | POST      | api/guardian/roles                                          | api.guardian.roles.store              | EmilMoe\Guardian\Guardian\API\RolesController@store              | auth              |
|        | GET|HEAD  | api/guardian/roles/{roles}                                  | api.guardian.roles.show               | EmilMoe\Guardian\Guardian\API\RolesController@show               | auth              |
|        | DELETE    | api/guardian/roles/{roles}                                  | api.guardian.roles.destroy            | EmilMoe\Guardian\Guardian\API\RolesController@destroy            | auth              |
|        | PUT|PATCH | api/guardian/roles/{roles}                                  | api.guardian.roles.update             | EmilMoe\Guardian\Guardian\API\RolesController@update             | auth              |
|        | PATCH     | api/guardian/roles/{roles}/permissions/add/{permissions}    | api.guardian.roles.permissions.add    | EmilMoe\Guardian\Guardian\API\Roles\PermissionsController@add    | auth              |
|        | PATCH     | api/guardian/roles/{roles}/permissions/remove/{permissions} | api.guardian.roles.permissions.remove | EmilMoe\Guardian\Guardian\API\Roles\PermissionsController@remove | auth              |
|        | PATCH     | api/guardian/roles/{roles}/users/add/{users}                | api.guardian.roles.users.add          | EmilMoe\Guardian\Guardian\API\Roles\UsersController@add          | auth              |
|        | PATCH     | api/guardian/roles/{roles}/users/remove/{users}             | api.guardian.roles.users.remove       | EmilMoe\Guardian\Guardian\API\Roles\UsersController@remove       | auth

There are some configurations related to the API

    /*
    |--------------------------------------------------------------------------
    | API
    |--------------------------------------------------------------------------
    |
    | The API gives you some basic URIs to access to perform common operations.
    | The API is currently optimal for installations which are not having
    | customers activated as the access endpoint currently don't check if
    | foreign table, user id and role id are associated with the current
    | customer.
    |
    | The URL is a prefix for all API calls. php artisan route:list will
    | describe more in detail what the URIs are.
    | Also the endpoint naming is affected by the url option.
    |
    */

    'api' => [
        'enabled' => true,
        'url'     => 'api/guardian',
    ],

Future implementations

CRUD

This is a nice feature I would like to add, but it's not important for getting the package up and running. It will allow separate permission for Index, Store, Show, Update, Delete.

Vue front end

I plan to ship a NPM package with a Vue front end, that easily allows to place a component on an admin interface page to administrate all the permissions. My goal with this is to have zero coding related to permissions.

I hope you will give me feedback, I will be happy to hear other opinions. Credits will be given :-)

Emil Moe

Expert in Laravel and Vue backed with MySQL databases. Independent developer who does freelance and love to travel. Feel free to drop me a message.

- Emil

Write your comment…

10 comments

Hi @emilmoe, this looks really good. I'm most excited about your SaaS support and Vue component as this could fit nicely into an install of Spark or other admin areas.

I agree you should change 'customers' to 'clients'.

Have you made the package public yet? How can we help you test this out?

Show all replies

Nice one @emilmoe. I will take a look soon!

Reply to this…

Share your programming knowledge and learn from the best developers on Hashnode

Get started

I have been working on the documentation for the package, it hopefully explains how the it's intended to work. I will release some source code as soon as it's ready for it.

Feel free to give me feedback on the project so far based on the documentation and if it needs to clarify something.

http://emilmoe.com/guardian/

Note that the repo is not working now as I haven't committed anything yet

Sorry for all the grammatical errors, I didn't have too much time to read it through and wanted to post it to let you know how it's going :-)

Reply to this…

you are about to get a guinea pig out of me for this. Very well done. Should I attempt this in Laravel 5.2?

Show all replies

There will not be anything that breaks in the minor versions 5.2.x, probably not in 5.1.x either. The newest feature it rely on is middleware parameters. Where do you think I should add a comment about it? There are still no automatic tests, only my trial and error.

Reply to this…