PermissionsAPI module
Facilitates protection of the app by allowing you to define authorization of actions on specific content (i.e models, etc...) via roles and permissions.
Integrates with Laravel Gate system so that permission checks can be done natively.
Relies heavily on spatie/laravel-permission
Quick start
Defining permissions is simple. You will need a permissions list and a definition to let the Permissions API know about it.
1. Create a Permissions folder and Permissions/Enums folder
Add a Permissions and Permissions/Enums folder to your module at Modules/[your-module]/Plugins/Permissions or to the app at app/Plugins/Permissions
2. List permission names for content to protect
- List permission names by creating a new Enum, e.g
YourContentPermissionsunderPermissions/Enums. Copy the example atPermissionsApi/Plugins/Permissions/Enums/RolePermissions. - Add a
SUBJECTconstant to refer to content to be protected, e.gyour_content. In case of a model, this should refer to the model name in snake case. - Add an
ACTION_ENUMSconstant with an array containing a list of action enums classes definedPermissions-Api/Plugins/Permissions/Enums/Actionsthat contain lists of common actions ('create', 'view', etc...). e.gconst ACTION_ENUMS = [PermissionActions::class,];
Permission names will then be generated dynamically using [SUBJECT].[ACTION] such as your_content.create.
3. Create a permissions definition
- Create a permissions definition under
Modules/[your-module]/Plugins/Permissionsorapp/Plugins/Permissions, e.gYourContentPermissionDefinitionCopy the example atPermissionsApi/Plugins/Permissions/RolePermissionDefinition. - Indicate the
groupyour permissions will be part of, used on UI for role permissions assignment. - Indicate a unique
typethat your permissions define, usually refer to theSUBJECTadded to the Permissions enum previously created. e.gYourContentPermissions::SUBJECT - Update
permissionsEnumwith permissions enum class previously created, e.gYourContentPermissions::class - Optionally add a
modelclass when protecting a model, e.gYourContent::class
4. Protect your content
The last step is to protect your content.
- Via your resource controller constructor:
$this->authorizeResource(YourModel::class, 'your_model_route_param');This will protect all standard resource routes. Any extra routes to be explicitly protected in your route declaration file - Or via route middleware:
Route::get('/', [Controller::class, 'index'])
->middleware('can:'.YourContentPermissionDefinition::SUBJECT.'.'.PermissionActions::VIEW)
And in case of models passed by route model binding:
Route::get('/{your_content}', [Controller::class, 'show'])
->middleware('can:'.PermissionActions::VIEW.',your_content')`
5. Restrict UI by permissions
- Whether declared in frontend or backend, restricted menu items should have a
permissionvalue, e.g{"text": "Dashboard", "url": "/dash", "permission": 'dashboard.view'} - Each menu should pass through the function
filterNavLinksPermissions(links)defined inMixins/PermissionHelperNote that the permissions list of each user is passed to the frontend via theSharedInertiaDatamiddleware andpermission_listattribute from TraitHasRoles.
6. Consider adding default role permissions
Extend app\Plugins\Permissions\Roles\AppDefaultRolePermissions by adding a new subject and actions for each role.
More info below under Default roles, migrations and seeding. Run the seeder and test.
7. Create tests
You can speed up Unit and Feature tests creation by extending PermissionsAPi\Tests\PermissionTestCase or simply using the handy trait PermissionsAPi\Tests\Trait\PermissionTestTrait.
In Depth.
Default roles, migrations and seeding.
Seeding
The PermissionsApi seeder will seed the full permissions list defined across all modules and application. It will also seed Default roles and default role permissions.
Default roles
- The PermissionsApi module comes with a default role
Super adminand default permissions for it. - You can define default roles at app level by extending the enum:
app\Plugins\Permissions\Roles\AppDefaultRoles. Simply add a new role as a constant.
Default role permissions
You can define default role permissions for each role at app level by extending the enum: app\Plugins\Permissions\Roles\AppDefaultRolePermissions.
Either:
- Add a new array to each role with a new subject that matches your Permissions subject (e.g
role) and an array of permission actions[create,view]. Permissions will be assigned to this role accordingly, e.grole.create,role.view - Or add a new array with a plain list of permissions, e.g
['do something different', 'another permission']
Subtypes.
Additional subjects can be added to permission names for more granular control over different variations of content.
These are called SUBTYPE in permissions enums.
For instance, settings.update.general will grant access to edit all general settings but not user settings, controlled by settings.update.user
These also answer to wild cards so granting settings.update will allow users to update all type of settings (equivalent to settings.update.*)
Wild cards.
Permission wildcards get generated from definitions to simplify permission assignment.
There are three types of wildcards used:
- Global, e.g
*users assigned this permission can do anything in the system, like super admins - Subject, e.g
*.create, users assigned this permission can create any content. - Action, e.g
role.*, users assigned this permission can do anything with roles. When setting up default permissions for a role, you for instance grant action wildcard permissions on a subject by adding:
[
'subject' => 'my_content',
'actions' => PermissionActions::ALL_ACTIONS
]
Custom permission names and actions.
You can add a custom list of permissions if pre-defined actions are not suitable.
- Ideally define a new set of actions as en enum under
YourModule/Plugins/Permissions/Enums/Actions/NewActionList. Add a new constant for each action, e.gconst ACCESS = 'access' - Add constants to your permissions enum (
YourContentPermissions) made from[ACTION]+[SUBJECT]. e.gconst ACCESS_YOURCONTENT = self::SUBJECT.'.'.NewActionList::ACCESS - Add your new action to the base model policy
PermissionsApi/Plugins/Permissions/Policies/BaseModelPolicy
Custom permission policies
Your permissions definition can use a custom permission policy for more complex business logic.
- Create a new policy under
YourModule/Plugins/Permissions/Policies/YourContentPolicyorYourModule/Plugins/Permissions/Policies/YourContentPolicy - Add the property
$policy = YourContentPolicy::classto your permissions definition (YourContentPermissionDefinition)
Protect content in functions (e.g in Controllers) rather than routes
You can use Laravel Gate Facade to protect your content. For example:
Gate::authorize(PermissionActions::VIEW, $model)will throw a 403 exception if current user is not authorized to view the model.- Use the
allowsmethod for conditional logicif (!Gate::allows(PermissionActions::VIEW, $model) { do something }. - If no models are involved, simply check the permission directly:
Gate::authorize(PermissionActions::VIEW .' '.YourContentPermissionDefinition::SUBJECT)
Permissions API Concepts explained
Permissions list build
- Permissions list gets accessed from a
PermissionsRepositoryInterface - Permissions lists are dynamically built from the app and modules by parsing "Permission definitions" files that extend
BasePermissionsDefinition - Each definition concerns a
typewhich is usually a model but can be something else such as a section of the app (e.gdashboard). - Modules and app can define as many permissions as required.
Permissions list in database
- The permissions list in the database is used to assign permissions to roles. It is maintained by a seeder called upon each deploy and looking for changes.
- Permissions are intentionally categorised by type and group to assist with UI building of role management pages.
Roles
- Permissions are not assigned to users directly. Permissions are assigned to roles and roles assigned to users.
- We check if users have
permissionto do something, we do not check roles since a user may cumulate multiple roles. As per permissions best practices
Model policies
- Most permission checks go through model permission policies that are mapped to each model.
- Policy mapping is done thanks to
$modeland$policyproperties specified in Permission definitions. - When a permission check is made, passing the model to the middleware or gate will tell Laravel to look at that policy for checks. e,g
Gate::allows(PermissionActions::VIEW, $model)will lookup the$modelpolicy. - In most cases, you should not worry about policies. The
BaseModelPolicyshould cover most cases. - Non model permissions do not use policies but check the permission directly using
Gate::beforelogic defined by Spatie/Laravel-permissions
Guidelines
- Ref. to full Laravel doc on Authorization: https://laravel.com/docs/9.x/authorization
- Permission names are defined using Enums to avoid hardcoded permission names, errors and promote reusability. We use the Laravel package BenSampo/laravel-enum to allow inheritance and for handy helpers.
- Use constants!