silvanite/novatoolpermissions
A Laravel Nova tool for managing roles and permissions directly from your admin panel. Assign permissions to roles and users through a clean Nova UI, register custom permissions, and keep access control organised without leaving Nova.
This package is a Nova tool that puts a CRUD UI over spatie/laravel-permission. The fastest way to understand it is to accept that split up front: Spatie owns the data and the enforcement, this package owns the screens. Everything below assumes Spatie is already (or is about to become) your single source of truth for roles and permissions.
Baseline requirements before you type composer require:
spatie/laravel-permission installed, migrations published, and its HasRoles trait on your User model.novafacile/slug field at ^1.2.3 — this is a hard constraint introduced in v1.0.9, not an optional nicety.composer require silvanite/novatoolpermissions --dry-run first. The last release is from April 2019; treat resolution as a coin flip until proven otherwise.Minimal install sequence:
composer require silvanite/novatoolpermissions
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan migrate
php artisan vendor:publish --tag=novatoolpermissions # config, if the tag exists in your version
Register the tool alongside your other Nova tools:
// app/Providers/NovaServiceProvider.php
public function tools()
{
return [
new \Silvanite\NovaToolPermissions\NovaToolPermissions(),
];
}
Then clear everything, because Nova caches menus and Spatie caches permissions:
php artisan config:clear && php artisan cache:clear && php artisan view:clear
Where to look first, in order:
vendor/silvanite/novatoolpermissions/src/ — the tool class, the resources it registers, and the middleware/policy it uses to gate tool visibility. This is small; read it all in ten minutes. You will need to know it precisely because upstream support does not exist.config/novatoolpermissions.php (if published) — for model overrides and any naming options.config/permission.php — the guard names here are the single most common cause of "the role exists but access is denied."Your first use case: log in as a super-admin, open the tool from the Nova sidebar, create a role (editor), create a permission (view posts), attach it, assign the role to a test user, then log in as that user and confirm both the in-app behaviour and which Nova tools they can see. If the second half doesn't change, you have a guard mismatch or a stale permission cache, not a broken package.
1. Developer-authored catalogue, admin-assigned roles. The healthiest split is: permissions are defined in code (a seeder or enum), roles are composed and assigned in the UI by non-developers. This keeps permission names stable — code references them by string, so an admin renaming publish posts to Publish Posts in the UI silently breaks every Gate::allows() that depended on it.
// database/seeders/PermissionSeeder.php
foreach (['view posts', 'create posts', 'publish posts', 'manage tools'] as $name) {
Permission::firstOrCreate(['name' => $name, 'guard_name' => 'web']);
}
Run it in every deploy. firstOrCreate makes it idempotent and non-destructive to whatever roles the admin has since configured.
2. Enforce with gates and policies, never with tool visibility. Tool hiding is a UI convenience. If a user knows the URL of a Nova tool or resource, hiding the menu entry stops nothing. Wire the actual boundary in your service provider:
Gate::before(fn ($user) => $user->hasRole('super-admin') ? true : null);
Gate::define('publish posts', fn ($user) => $user->hasPermissionTo('publish posts'));
Gate::before returning true for super-admins is the standard Spatie idiom and saves you from enumerating every permission for the one account that must never be locked out.
3. Conditionally register Nova tools. The most portable pattern — independent of whatever the package's internal visibility mechanism does — is to decide at registration time:
public function tools()
{
return collect([
new \Silvanite\NovaToolPermissions\NovaToolPermissions(),
new \App\Nova\Tools\Reports(),
])->filter(fn ($tool) => Gate::allows('manage tools') || ! $tool instanceof \App\Nova\Tools\Reports)
->values()->all();
}
Prefer an explicit canSee()/authorize() on your own Tool subclasses for anything you write yourself; use the package's UI only for the tools it natively understands.
4. Gate the permissions tool itself. The tool that edits permissions is the highest-value target in the panel. Restrict it to a named role, and keep that role out of the UI's reach:
// In the Tool subclass or via a Gate check in NovaServiceProvider
public function authorizedToSee(Request $request)
{
return $request->user()->hasRole('super-admin');
}
At minimum, ensure no role created through the UI can grant manage tools to itself.
5. Drive Nova resources from the same permissions. Consistency between "can they see the tool" and "can they see the resource" matters more than either individually:
public static function authorizedToViewAny(Request $request)
{
return $request->user()->can('view posts');
}
6. Test the permission boundary, not the UI. Feature-test the enforcement layer with Spatie's helpers; the Nova screens are not worth asserting on given their age.
public function test_editor_cannot_publish()
{
$user = User::factory()->create();
$user->assignRole('editor');
$this->actingAs($user)
->post('/posts/1/publish')
->assertForbidden();
}
Call app(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions() in setUp() if you create permissions mid-test, or you will chase phantoms.
7. Deploy checklist. Permissions are cached, so any change to roles or permissions in a deploy needs a cache reset: php artisan permission:cache-reset (or cache:clear). Nova caches its tool/resource manifest too — bounce it with nova:publish/optimize:clear if the sidebar looks stale after a deploy.
The package is dormant — plan accordingly. Last release April 2019, ~100 stars, zero dependents, effectively one maintainer. It will not receive fixes for a newer Nova major or a current Spatie release. Pin the exact versions in composer.json, and confirm with composer require --dry-run against your real stack before committing. Decide now who forks it if it breaks; the MIT licence and small source surface make that a days-not-months job, but only if someone is actually named.
The v1.0.9 slug change is the known landmine. A patch release changed the slug parameter from accepting the field name to requiring the attribute name, and bumped the slug field requirement to ^1.2.3. The failure mode is silent: wrong or missing slugs rather than an exception. Grep your resources, seeders, imports, and API payloads for slug, fix any that pass a field name, and add a regression test that asserts stable slugs on role/permission create and update.
Guard mismatch is the number-one "it doesn't work" cause. Spatie stores guard_name on every role and permission. If your Nova guard differs from your web guard and you seeded permissions under the wrong one, hasPermissionTo() returns false with no error. Set guard_name explicitly in seeders rather than relying on the default, and if you run multiple guards, verify each permission is created per-guard.
Caches hide everything. Three separate caches can each make a correct change look broken: Spatie's permission cache, Laravel's config cache, and Nova's manifest. When behaviour contradicts the database, clear all three before debugging code.
Tool hiding is not authorization. It removes the menu item; it does not remove the route. Any tool that does something sensitive must enforce its own gate inside the controller or the Tool class. Treat the package's visibility feature as an admin-UX improvement only, and never as the reason a sensitive capability is "safe."
Don't rename permissions through the UI. Strings in code are your contract. If permission names must change, change them in a seeder plus a migration that updates references, and search for every can('…'), hasPermissionTo('…'), and middleware string. Consider an enum-backed constants class so at least the codebase fails loudly at the compiler level.
No multi-tenancy, no audit trail, no approval workflow. The package edits Spatie's tables directly, so if you need teams, per-tenant roles, or a log of who granted what, you are building that on top regardless — either as extra columns on the pivot tables plus a model observer, or as a replacement UI. Budget for it rather than discovering it after rollout.
Extension points, in order of usefulness: (a) Spatie's config for models and table names; (b) Gate::before for super-admins; (c) conditional tool registration in NovaServiceProvider; (d) forking the tool's resources if you need extra columns. Reach for (d) last — every line you fork is a line you now maintain.
Debugging sequence when access is wrong: check model_has_roles / role_has_permissions in the DB → check guard_name on both sides → php artisan permission:cache-reset → verify the gate is actually evaluated (dump inside Gate::define) → only then suspect the Nova UI. In practice the UI is rarely the culprit; the data and the cache are.
How can I help you explore Laravel packages today?