Objectives:
Out of the box, Twill has 4 possible roles:
In this example, we'll create a new Author
role, as well as 2 new modules: Pages
and Posts
. Then, we'll limit the
access of the Author
role to the Posts
module only.
1php artisan twill:make:module posts2php artisan twill:make:module pages
Run the migrations:
1php artisan migrate
Add the modules to our admin routes:
File:
routes/twill.php
1use A17\Twill\Facades\TwillRoutes;2 3TwillRoutes::module('pages');4 5TwillRoutes::module('posts');
... and to our navigation:
File:
config/twill-navigation.php
1return [ 2 'pages' => [ 3 'title' => 'Pages', 4 'module' => true, 5 ], 6 'posts' => [ 7 'title' => 'Posts', 8 'module' => true, 9 ],10];
To define a new role, we need to override a file from within the Twill package. This can be done in a few steps, via composer configuration. Let's start by defining the new role in the context of Twill default roles:
File:
app/Models/Enums/UserRole.php
1<?php 2 3namespace A17\Twill\Models\Enums; 4 5use MyCLabs\Enum\Enum; 6 7class UserRole extends Enum 8{ 9 const VIEWONLY = 'View only';10 const AUTHOR = 'Author';11 const PUBLISHER = 'Publisher';12 const ADMIN = 'Admin';13}
Conceptually, Authors are just below Publishers in terms of access-level. Publishers are able to create and update all types of modules, but Authors are restricted to Posts only.
Then, let's update our composer autoload configuration:
File:
composer.json
1"autoload": {2 ...34 "files": ["app/Models/Enums/UserRole.php"],5 "exclude-from-classmap": ["vendor/area17/twill/src/Models/Enums/UserRole.php"]6}
This tells composer to effectively replace Twill's file by the one we added in our project.
To enable the override, run:
1composer dump-autoload
Next, we'll log into Twill as SuperAdmin and create 2 new users:
After activating each user account, you'll notice that Alice has access to everything and that Bob has access to... pretty much nothing, exept his own user profile. Let's keep going!
Like a standard Laravel application, Twill defines its permissions through gates in an AuthServiceProvider
class. In
the same way, let's define 2 new permissions in our project:
File:
app/Providers/AuthServiceProvider.php
1use A17\Twill\Models\Enums\UserRole; 2 3class AuthServiceProvider extends ServiceProvider 4{ 5 // ... 6 7 public function boot() 8 { 9 $this->registerPolicies();10 11 // The `list-posts` permission is granted to users of all roles12 Gate::define('list-posts', function ($user) {13 if ($user->isSuperAdmin()) {14 return true;15 }16 17 return in_array($user->role_value, [18 UserRole::VIEWONLY,19 UserRole::AUTHOR,20 UserRole::PUBLISHER,21 UserRole::ADMIN,22 ]);23 });24 25 // The `edit-posts` permission is granted to users of all roles, except `View Only`26 Gate::define('edit-posts', function ($user) {27 if ($user->isSuperAdmin()) {28 return true;29 }30 31 return in_array($user->role_value, [32 UserRole::AUTHOR,33 UserRole::PUBLISHER,34 UserRole::ADMIN,35 ]);36 });37 }38}
Then, we'll apply the list-posts
permission to the Posts
navigation item:
File:
config/twill-navigation.php
1 2'posts' => [3 'title' => 'Posts',4 'module' => true,5 'can' => 'list-posts',6],
With this, Bob can now see the Posts
item in the Twill navigation. However, Bob is getting a Forbidden!
error
message when trying to access it. Almost there!
We have applied the list-posts
permission to the navigation, but what about the edit-posts
permission? We'll need to
override 2 methods from the base ModuleController
class to finish:
File:
app/Http/Controllers/Admin/PostController.php
1class PostController extends ModuleController 2{ 3 // ... 4 5 /** 6 * On the base ModuleController, this is where built-in gates are assigned to each 7 * controller action. Our `posts` module is only interested in our 2 custom gates, 8 * for all possible actions. 9 */10 protected function setMiddlewarePermission()11 {12 $this->middleware('can:list-posts', ['only' => ['index', 'show']]);13 $this->middleware('can:edit-posts', ['except' => ['index', 'show']]);14 }15 16 /**17 * On the base ModuleController, this is also used to assign built-in gates to specific18 * options defined in the `$indexOptions` array. In this simplified example, all index19 * options require the `edit-posts` permission.20 */21 protected function getIndexOption($option)22 {23 if (! \Auth::guard('twill_users')->user()->can('edit-posts')) {24 return false;25 }26 27 return ($this->indexOptions[$option] ?? $this->defaultIndexOptions[$option] ?? false);28 }29}
This effectively removes all Twill built-in gates for our module.
And there we have it, a new role is now available in our system!
If your modules don't need to differentiate between list
and edit
permissions, you can move the middleware settings
to your admin routes instead of the controllers.
First, disable Twill's built-in gates on the module's controller:
File:
app/Http/Controllers/Admin/PostController.php
1class PostController extends ModuleController 2{ 3 // ... 4 5 protected function setMiddlewarePermission() 6 { 7 } 8 9 protected function getIndexOption($option)10 {11 return ($this->indexOptions[$option] ?? $this->defaultIndexOptions[$option] ?? false);12 }13}
Then, add the route groups and middleware in the admin routes configuration:
File:
routes/twill.php
1use A17\Twill\Facades\TwillRoutes; 2 3Route::group(['middleware' => 'can:edit-pages'], function () { 4 TwillRoutes::module('pages'); 5}); 6 7Route::group(['middleware' => 'can:edit-posts'], function () { 8 TwillRoutes::module('posts'); 9});10 11// ...
Route groups are especially useful if you want to define global permission groups for multiple modules (
e.g. can:edit-blog
, can:edit-site-content
, etc.)
Our AuthServiceProvider
is functional but as we keep adding permissions, we can see that we'll end up with a quite a
bit of duplication.
To clean things up, we can use class constants to group common roles. We can also extend Twill's
own AuthServiceProvider
class, which has two utility methods: authorize()
and userHasrole()
:
File:
app/Providers/AuthServiceProvider.php
1use A17\Twill\AuthServiceProvider as TwillAuthServiceProvider; 2 3class AuthServiceProvider extends TwillAuthServiceProvider 4{ 5 const ALL_ROLES = [UserRole::VIEWONLY, UserRole::AUTHOR, UserRole::PUBLISHER, UserRole::ADMIN]; 6 const ALL_EDITORS = [UserRole::AUTHOR, UserRole::PUBLISHER, UserRole::ADMIN]; 7 8 public function boot() 9 {10 // `pages` module permissions11 Gate::define('list-pages', function ($user) {12 return $this->authorize($user, function ($user) {13 return $this->userHasRole($user, [UserRole::VIEWONLY, UserRole::PUBLISHER, UserRole::ADMIN]);14 });15 });16 Gate::define('edit-pages', function ($user) {17 return $this->authorize($user, function ($user) {18 return $this->userHasRole($user, [UserRole::PUBLISHER, UserRole::ADMIN]);19 });20 });21 22 // `posts` module permissions23 Gate::define('list-posts', function ($user) {24 return $this->authorize($user, function ($user) {25 return $this->userHasRole($user, self::ALL_ROLES);26 });27 });28 Gate::define('edit-posts', function ($user) {29 return $this->authorize($user, function ($user) {30 return $this->userHasRole($user, self::ALL_EDITORS);31 });32 });33 }34}
At this point, if you have experimented a bit with posts and pages in your project, you may have noticed that authors
don't have access to the Media Library. Also in forms, authors can see the medias
fields but can't add or change the
selected images in them.
Just like listing and editing modules, Twill has a few built-in gates to handle the Media Library permissions:
list
permission is needed to browse the Media
Library (see _global_navigation.blade.php)upload
permission is needed to display the Add new
button (see layouts/main.blade.php)edit
permission is also needed to process and save the uploaded
images (see MediaLibraryController.php)Here's our revised AuthServiceProvider
to give authors full access to the Media Library:
File:
app/Providers/AuthServiceProvider.php
1class AuthServiceProvider extends TwillAuthServiceProvider 2{ 3 const ALL_ROLES = [UserRole::VIEWONLY, UserRole::AUTHOR, UserRole::PUBLISHER, UserRole::ADMIN]; 4 const ALL_EDITORS = [UserRole::AUTHOR, UserRole::PUBLISHER, UserRole::ADMIN]; 5 6 public function boot() 7 { 8 // `list` permission is needed to access the Media Library 9 Gate::define('list', function ($user) {10 return $this->authorize($user, function ($user) {11 return $this->userHasRole($user, self::ALL_ROLES);12 });13 });14 15 // `upload` and `edit` permissions are needed to upload to the Media Library16 Gate::define('upload', function ($user) {17 return $this->authorize($user, function ($user) {18 return $this->userHasRole($user, self::ALL_EDITORS);19 });20 });21 Gate::define('edit', function ($user) {22 return $this->authorize($user, function ($user) {23 return $this->userHasRole($user, self::ALL_EDITORS);24 });25 });26 27 // `pages` module permissions28 Gate::define('list-pages', function ($user) {29 return $this->authorize($user, function ($user) {30 return $this->userHasRole($user, [UserRole::VIEWONLY, UserRole::PUBLISHER, UserRole::ADMIN]);31 });32 });33 Gate::define('edit-pages', function ($user) {34 return $this->authorize($user, function ($user) {35 return $this->userHasRole($user, [UserRole::PUBLISHER, UserRole::ADMIN]);36 });37 });38 39 // `posts` module permissions40 Gate::define('list-posts', function ($user) {41 return $this->authorize($user, function ($user) {42 return $this->userHasRole($user, self::ALL_ROLES);43 });44 });45 Gate::define('edit-posts', function ($user) {46 return $this->authorize($user, function ($user) {47 return $this->userHasRole($user, self::ALL_EDITORS);48 });49 });50 }51}
Important Because list
and edit
are global permissions in Twill, when giving access to the Media Library, you
may need to add or adjust custom permissions on other modules to preserve the correct access levels.
We barely scratched the surface in terms of what is possible with gates within a Laravel project. You can learn more in Laravel's Autorization documentation
With this, you have a good understanding of how permissions work within Twill. You can explore all the default gates that are defined in Twill's AuthServiceProvider. You can use this as a base to extend or change Twill's built-in permissions.
The base ModuleController class is also a great place to look for more fine-grained control over specific controller actions. (e.g. Allow certain users to create and edit, but not to delete).
A complete overhaul of Twill's permissions system is being finalized for Twill 3.0. It adds user roles, groups and item-level permissions, all configurable from within the CMS. You can find more information in the following pull request (#1138), including notes on how to test this new feature on a new project.
Thanks for reading and have fun :)